From 0e10866fb5c12e1e4fdadd41683fcfa4a0a27f3a Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 18 Jan 2021 11:02:16 +0900 Subject: [PATCH 01/44] various insets fixes --- .../collection/thumbnail_collection.dart | 1 + .../common/gesture_area_protector.dart | 23 +++-- lib/widgets/drawer/app_drawer.dart | 84 ++++++++++--------- .../filter_grids/common/filter_grid_page.dart | 1 + lib/widgets/search/search_page.dart | 9 +- lib/widgets/viewer/info/info_page.dart | 1 + lib/widgets/viewer/panorama_page.dart | 6 +- 7 files changed, 73 insertions(+), 52 deletions(-) diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index f5530347b..cdc36a635 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -44,6 +44,7 @@ class ThumbnailCollection extends StatelessWidget { Widget build(BuildContext context) { return HighlightInfoProvider( child: SafeArea( + bottom: false, child: LayoutBuilder( builder: (context, constraints) { final viewportSize = constraints.biggest; diff --git a/lib/widgets/common/gesture_area_protector.dart b/lib/widgets/common/gesture_area_protector.dart index 33a736d6b..0fb879ded 100644 --- a/lib/widgets/common/gesture_area_protector.dart +++ b/lib/widgets/common/gesture_area_protector.dart @@ -1,4 +1,7 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; // This widget should be added on top of Scaffolds with: // - `resizeToAvoidBottomInset` set to false, @@ -11,12 +14,20 @@ class BottomGestureAreaProtector extends StatelessWidget { @override Widget build(BuildContext context) { - return Positioned( - left: 0, - right: 0, - bottom: 0, - height: systemGestureInsetsBottom, - child: AbsorbPointer(), + return Selector( + selector: (c, mq) => max(mq.viewPadding.bottom, mq.viewInsets.bottom), + builder: (c, mqPaddingBottom, child) { + // devices with physical navigation buttons have no bottom insets + // we assume these devices do not use gesture navigation + if (mqPaddingBottom == 0) return SizedBox(); + return Positioned( + left: 0, + right: 0, + bottom: 0, + height: systemGestureInsetsBottom, + child: AbsorbPointer(), + ); + }, ); } } diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index 1daea1536..22358d397 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:ui'; import 'package:aves/model/filters/album.dart'; @@ -38,10 +39,52 @@ class _AppDrawerState extends State { @override Widget build(BuildContext context) { - final header = Container( + final drawerItems = [ + _buildHeader(context), + allCollectionTile, + videoTile, + favouriteTile, + _buildSpecialAlbumSection(), + Divider(), + albumListTile, + countryListTile, + tagListTile, + Divider(), + settingsTile, + aboutTile, + if (kDebugMode) ...[ + Divider(), + debugTile, + ], + ]; + + return Drawer( + child: Selector( + selector: (c, mq) => max(mq.viewPadding.bottom, mq.viewInsets.bottom), + builder: (c, mqPaddingBottom, child) { + return SingleChildScrollView( + padding: EdgeInsets.only(bottom: mqPaddingBottom), + child: Theme( + data: Theme.of(context).copyWith( + // color used by `ExpansionTile` for leading icon + unselectedWidgetColor: Colors.white, + ), + child: Column( + children: drawerItems, + ), + ), + ); + }, + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Container( padding: EdgeInsets.all(16), color: Theme.of(context).accentColor, child: SafeArea( + bottom: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -66,45 +109,6 @@ class _AppDrawerState extends State { ), ), ); - - final drawerItems = [ - header, - allCollectionTile, - videoTile, - favouriteTile, - _buildSpecialAlbumSection(), - Divider(), - albumListTile, - countryListTile, - tagListTile, - Divider(), - settingsTile, - aboutTile, - if (kDebugMode) ...[ - Divider(), - debugTile, - ], - ]; - - return Drawer( - child: Selector( - selector: (c, mq) => mq.viewInsets.bottom, - builder: (c, mqViewInsetsBottom, child) { - return SingleChildScrollView( - padding: EdgeInsets.only(bottom: mqViewInsetsBottom), - child: Theme( - data: Theme.of(context).copyWith( - // color used by `ExpansionTile` for leading icon - unselectedWidgetColor: Colors.white, - ), - child: Column( - children: drawerItems, - ), - ), - ); - }, - ), - ); } Widget _buildAlbumTile(String album) { diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index b03997e9b..42387d473 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -72,6 +72,7 @@ class FilterGridPage extends StatelessWidget { child: HighlightInfoProvider( child: GestureAreaProtectorStack( child: SafeArea( + bottom: false, child: LayoutBuilder( builder: (context, constraints) { final viewportSize = constraints.biggest; diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/search/search_page.dart index ef3c3e933..e85ced83d 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/search/search_page.dart @@ -71,9 +71,12 @@ class _SearchPageState extends State { } void _onQueryChanged() { - _debouncer(() => setState(() { - // rebuild ourselves because query changed. - })); + _debouncer(() { + if (mounted) { + // rebuild ourselves because query changed. + setState(() {}); + } + }); } void _onSearchBodyChanged() { diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 7f6bf741c..14364b425 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -43,6 +43,7 @@ class _InfoPageState extends State { child: Scaffold( body: GestureAreaProtectorStack( child: SafeArea( + bottom: false, child: NotificationListener( onNotification: _handleTopScroll, child: Selector>( diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index d1e44385b..2eb5d03fb 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -97,10 +97,10 @@ class _PanoramaPageState extends State { return Visibility( visible: overlayVisible, child: Selector( - selector: (c, mq) => mq.padding + mq.viewInsets, - builder: (c, mqViewInsets, child) { + selector: (c, mq) => mq.viewPadding + mq.viewInsets, + builder: (c, mqPadding, child) { return Padding( - padding: EdgeInsets.all(8) + EdgeInsets.only(right: mqViewInsets.right, bottom: mqViewInsets.bottom), + padding: EdgeInsets.all(8) + EdgeInsets.only(right: mqPadding.right, bottom: mqPadding.bottom), child: OverlayButton( scale: kAlwaysCompleteAnimation, child: ValueListenableBuilder( From 67130c30baba75d1fcb3d6d1be4e73bfa201c7b9 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 18 Jan 2021 11:06:48 +0900 Subject: [PATCH 02/44] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 52ed6d473..fbb336a5a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt ## Features -- support raster images: JPEG, GIF, PNG, HEIC (from Android Pie), WEBP, TIFF, BMP, WBMP, ICO +- support raster images: JPEG, GIF, PNG, HEIC (from Android Pie), WEBP, TIFF (including multi-page), BMP, WBMP, ICO - support animated images: GIF, WEBP - support raw images: ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW - support vector images: SVG @@ -36,10 +36,10 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt | Model | Name | Android Version | API | | ----------- | -------------------------- | --------------- | ---:| -| SM-G970N | Samsung Galaxy S10e | 10 (Android10) | 29 | +| SM-G981N | Samsung Galaxy S20 5G | 11 | 30 | +| SM-G970N | Samsung Galaxy S10e | 10 (Q) | 29 | | SM-P580 | Samsung Galaxy Tab A 10.1 | 8.1.0 (Oreo) | 27 | | SM-G930S | Samsung Galaxy S7 | 8.0.0 (Oreo) | 26 | -| E5823 | Sony Xperia Z5 Compact | 7.1.1 (Nougat) | 25 | ## Project Setup From a8bab930359522f1c4adfb1a64fe7bd7b9c4d630 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 18 Jan 2021 11:48:57 +0900 Subject: [PATCH 03/44] catalog: hide `unregistered schema` exceptions when using non standard namespaces --- .../deckers/thibault/aves/metadata/XMP.kt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index 1e67bdfa4..bd7ff1733 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -1,6 +1,7 @@ package deckers.thibault.aves.metadata import android.util.Log +import com.adobe.internal.xmp.XMPError import com.adobe.internal.xmp.XMPException import com.adobe.internal.xmp.XMPMeta import deckers.thibault.aves.utils.LogUtils @@ -9,6 +10,8 @@ import java.util.* object XMP { private val LOG_TAG = LogUtils.createTag(XMP::class.java) + // standard namespaces + // cf com.adobe.internal.xmp.XMPConst const val DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/" const val PHOTOSHOP_SCHEMA_NS = "http://ns.adobe.com/photoshop/1.0/" const val XMP_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/" @@ -69,9 +72,27 @@ object XMP { fun XMPMeta.isPanorama(): Boolean { // Google - if (gpanoRequiredProps.all { doesPropertyExist(GPANO_SCHEMA_NS, it) }) return true + try { + if (gpanoRequiredProps.all { doesPropertyExist(GPANO_SCHEMA_NS, it) }) return true + } catch (e: XMPException) { + if (e.errorCode != XMPError.BADSCHEMA) { + // `BADSCHEMA` code is reported when we check a property + // from a non standard namespace, and that namespace is not declared in the XMP + Log.w(LOG_TAG, "failed to check Google panorama props from XMP", e) + } + } + // Photomatix - if (getPropertyString(PMTM_SCHEMA_NS, PMTM_IS_PANO360) == "Yes") return true + try { + if (getPropertyString(PMTM_SCHEMA_NS, PMTM_IS_PANO360) == "Yes") return true + } catch (e: XMPException) { + if (e.errorCode != XMPError.BADSCHEMA) { + // `BADSCHEMA` code is reported when we check a property + // from a non standard namespace, and that namespace is not declared in the XMP + Log.w(LOG_TAG, "failed to check Photomatix panorama props from XMP", e) + } + } + return false } @@ -102,7 +123,7 @@ object XMP { } } } catch (e: XMPException) { - Log.w(LOG_TAG, "failed to get text for XMP schema=$schema, propName=$propName", e) + Log.w(LOG_TAG, "failed to get date for XMP schema=$schema, propName=$propName", e) } } } \ No newline at end of file From 4690fac4f6092c9a26bdf0a23264307e5a07f220 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 18 Jan 2021 11:59:22 +0900 Subject: [PATCH 04/44] minor fixes --- lib/widgets/common/grid/header.dart | 4 ++-- lib/widgets/common/scaling.dart | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index 0bc1db5ad..3acf4c2af 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -36,7 +36,7 @@ class SectionHeader extends StatelessWidget { padding: padding, constraints: BoxConstraints(minHeight: leadingDimension), child: GestureDetector( - onTap: () => _toggleSectionSelection(context), + onTap: selectable ? () => _toggleSectionSelection(context) : null, child: Text.rich( TextSpan( children: [ @@ -53,7 +53,7 @@ class SectionHeader extends StatelessWidget { child: leading, ) : null, - onPressed: () => _toggleSectionSelection(context), + onPressed: selectable ? () => _toggleSectionSelection(context) : null, ), ), TextSpan( diff --git a/lib/widgets/common/scaling.dart b/lib/widgets/common/scaling.dart index 6e74ed431..10b479fc1 100644 --- a/lib/widgets/common/scaling.dart +++ b/lib/widgets/common/scaling.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'dart:ui' as ui; import 'package:aves/theme/durations.dart'; @@ -153,7 +152,11 @@ class _GridScaleGestureDetectorState extends State Date: Tue, 19 Jan 2021 10:24:31 +0900 Subject: [PATCH 05/44] viewer: multitrack HEIF support --- .../aves/channel/calls/MetadataHandler.kt | 48 ++++++- .../aves/channel/calls/ThumbnailFetcher.kt | 16 ++- .../channel/streams/ImageByteStreamHandler.kt | 20 ++- .../decoder/MultiTrackThumbnailGlideModule.kt | 124 ++++++++++++++++++ .../deckers/thibault/aves/utils/MimeTypes.kt | 9 +- lib/image_providers/region_provider.dart | 12 +- lib/image_providers/thumbnail_provider.dart | 13 +- lib/image_providers/uri_image_provider.dart | 6 +- lib/model/entry_cache.dart | 6 +- lib/model/image_entry.dart | 62 +++++---- lib/model/image_metadata.dart | 10 +- lib/model/multipage.dart | 82 +++++++++--- lib/services/image_file_service.dart | 6 +- lib/widgets/collection/thumbnail/raster.dart | 10 +- lib/widgets/collection/thumbnail/vector.dart | 2 +- lib/widgets/viewer/debug_page.dart | 2 +- lib/widgets/viewer/entry_scroller.dart | 8 +- lib/widgets/viewer/info/basic_section.dart | 2 +- lib/widgets/viewer/overlay/bottom.dart | 87 ++++++------ lib/widgets/viewer/overlay/minimap.dart | 9 +- lib/widgets/viewer/overlay/multipage.dart | 56 +++++--- lib/widgets/viewer/overlay/top.dart | 2 +- lib/widgets/viewer/panorama_page.dart | 4 +- lib/widgets/viewer/printer.dart | 2 +- .../viewer/visual/entry_page_view.dart | 29 ++-- lib/widgets/viewer/visual/raster.dart | 22 +--- lib/widgets/viewer/visual/video.dart | 2 +- 27 files changed, 440 insertions(+), 211 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackThumbnailGlideModule.kt diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index f0a95b2d6..e1d693a83 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -1,6 +1,8 @@ package deckers.thibault.aves.channel.calls import android.content.Context +import android.media.MediaExtractor +import android.media.MediaFormat import android.media.MediaMetadataRetriever import android.net.Uri import android.util.Log @@ -45,6 +47,7 @@ import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MimeTypes.isHeifLike import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isMultimedia import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface @@ -430,7 +433,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } } - if (mimeType == MimeTypes.HEIC || mimeType == MimeTypes.HEIF) { + if (isHeifLike(mimeType)) { retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS) { if (it > 1) flags = flags or MASK_IS_MULTIPAGE } @@ -525,8 +528,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { if (mimeType == MimeTypes.TIFF) { fun toMap(options: TiffBitmapFactory.Options): Map { return hashMapOf( - "width" to options.outWidth, - "height" to options.outHeight, + KEY_MIME_TYPE to mimeType, + KEY_WIDTH to options.outWidth, + KEY_HEIGHT to options.outHeight, ) } getTiffPageInfo(uri, 0)?.let { first -> @@ -536,6 +540,36 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { getTiffPageInfo(uri, i)?.let { pages[i] = toMap(it) } } } + } else if (isHeifLike(mimeType)) { + fun MediaFormat.getSafeInt(key: String, save: (value: Int) -> Unit) { + if (this.containsKey(key)) save(this.getInteger(key)) + } + + fun MediaFormat.getSafeLong(key: String, save: (value: Long) -> Unit) { + if (this.containsKey(key)) save(this.getLong(key)) + } + + val extractor = MediaExtractor() + extractor.setDataSource(context, uri, null) + for (i in 0 until extractor.trackCount) { + try { + val format = extractor.getTrackFormat(i) + format.getString(MediaFormat.KEY_MIME)?.let { mime -> + val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime + val page = hashMapOf(KEY_MIME_TYPE to trackMime) + format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it } + format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it } + format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it } + if (isVideo(trackMime)) { + format.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 } + } + pages[i] = page + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get track information for uri=$uri, track num=$i", e) + } + } + extractor.release() } result.success(pages) } @@ -619,7 +653,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) exif.thumbnailBitmap?.let { bitmap -> TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let { - it.getBytes(canHaveAlpha = false, recycle = false)?.let { thumbnails.add(it) } + it.getBytes(canHaveAlpha = false, recycle = false)?.let { bytes -> thumbnails.add(bytes) } } } } @@ -733,7 +767,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private val LOG_TAG = LogUtils.createTag(MetadataHandler::class.java) const val CHANNEL = "deckers.thibault/aves/metadata" - // catalog metadata + // catalog metadata & page info private const val KEY_MIME_TYPE = "mimeType" private const val KEY_DATE_MILLIS = "dateMillis" private const val KEY_FLAGS = "flags" @@ -742,6 +776,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private const val KEY_LONGITUDE = "longitude" private const val KEY_XMP_SUBJECTS = "xmpSubjects" private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription" + private const val KEY_HEIGHT = "height" + private const val KEY_WIDTH = "width" + private const val KEY_TRACK_ID = "trackId" + private const val KEY_DURATION = "durationMillis" private const val MASK_IS_ANIMATED = 1 shl 0 private const val MASK_IS_FLIPPED = 1 shl 1 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt index 3728fa209..57fa44102 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt @@ -13,11 +13,13 @@ import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.signature.ObjectKey +import deckers.thibault.aves.decoder.MultiTrackThumbnail import deckers.thibault.aves.decoder.TiffThumbnail import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MimeTypes.isHeifLike import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide @@ -32,14 +34,16 @@ class ThumbnailFetcher internal constructor( private val isFlipped: Boolean, width: Int?, height: Int?, - page: Int?, + private val page: Int?, private val defaultSize: Int, private val result: MethodChannel.Result, ) { private val uri: Uri = Uri.parse(uri) private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize - private val page = page ?: 0 + private val tiffFetch = mimeType == MimeTypes.TIFF + private val multiTrackFetch = isHeifLike(mimeType) && page != null + private val customFetch = tiffFetch || multiTrackFetch fun fetch() { var bitmap: Bitmap? = null @@ -47,7 +51,7 @@ class ThumbnailFetcher internal constructor( var exception: Exception? = null try { - if (mimeType != MimeTypes.TIFF && (width == defaultSize || height == defaultSize) && !isFlipped) { + if (!customFetch && (width == defaultSize || height == defaultSize) && !isFlipped) { // Fetch low quality thumbnails when size is not specified. // As of Android R, the Media Store content resolver may return a thumbnail // that is automatically rotated according to EXIF orientation, but not flipped, @@ -121,7 +125,11 @@ class ThumbnailFetcher internal constructor( .load(VideoThumbnail(context, uri)) .submit(width, height) } else { - val model: Any = if (mimeType == MimeTypes.TIFF) TiffThumbnail(context, uri, page) else uri + val model: Any = if (tiffFetch) { + TiffThumbnail(context, uri, page ?: 0) + } else if (multiTrackFetch) { + MultiTrackThumbnail(context, uri, page ?: 0) + } else uri Glide.with(context) .asBitmap() .apply(options) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt index 04087ca04..1f3a155de 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -9,11 +9,13 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions +import deckers.thibault.aves.decoder.MultiTrackThumbnail import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MimeTypes.isHeifLike import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide @@ -84,7 +86,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) } val rotationDegrees = arguments["rotationDegrees"] as Int val isFlipped = arguments["isFlipped"] as Boolean - val page = arguments["page"] as Int + val page = arguments["page"] as Int? if (mimeType == null || uri == null) { error("streamImage-args", "failed because of missing arguments", null) @@ -98,7 +100,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen streamTiffImage(uri, page) } else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) { // decode exotic format on platform side, then encode it in portable format for Flutter - streamImageByGlide(uri, mimeType, rotationDegrees, isFlipped) + streamImageByGlide(uri, page, mimeType, rotationDegrees, isFlipped) } else { // to be decoded by Flutter streamImageAsIs(uri) @@ -114,11 +116,17 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } } - private fun streamImageByGlide(uri: Uri, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { + private fun streamImageByGlide(uri: Uri, page: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { + val model: Any = if (isHeifLike(mimeType) && page != null) { + MultiTrackThumbnail(activity, uri, page) + } else { + uri + } + val target = Glide.with(activity) .asBitmap() .apply(glideOptions) - .load(uri) + .load(model) .submit() try { var bitmap = target.get() @@ -157,7 +165,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } } - private fun streamTiffImage(uri: Uri, page: Int = 0) { + private fun streamTiffImage(uri: Uri, page: Int?) { val resolver = activity.contentResolver try { val fd = resolver.openFileDescriptor(uri, "r")?.detachFd() @@ -166,7 +174,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen return } val options = TiffBitmapFactory.Options().apply { - inDirectoryNumber = page + inDirectoryNumber = page ?: 0 } val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) if (bitmap != null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackThumbnailGlideModule.kt new file mode 100644 index 000000000..3d96d8b61 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackThumbnailGlideModule.kt @@ -0,0 +1,124 @@ +package deckers.thibault.aves.decoder + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaExtractor +import android.media.MediaFormat +import android.net.Uri +import android.os.Build +import android.util.Log +import com.bumptech.glide.Glide +import com.bumptech.glide.Priority +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.data.DataFetcher.DataCallback +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.module.LibraryGlideModule +import com.bumptech.glide.signature.ObjectKey +import deckers.thibault.aves.utils.BitmapUtils.getBytes +import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.StorageUtils +import java.io.InputStream + + +@GlideModule +class MultiTrackThumbnailGlideModule : LibraryGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.append(MultiTrackThumbnail::class.java, InputStream::class.java, MultiTrackThumbnailLoader.Factory()) + } +} + +class MultiTrackThumbnail(val context: Context, val uri: Uri, val trackIndex: Int) + +internal class MultiTrackThumbnailLoader : ModelLoader { + override fun buildLoadData(model: MultiTrackThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData { + return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackThumbnailFetcher(model, width, height)) + } + + override fun handles(MultiTrackThumbnail: MultiTrackThumbnail): Boolean = true + + internal class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = MultiTrackThumbnailLoader() + + override fun teardown() {} + } +} + +internal class MultiTrackThumbnailFetcher(val model: MultiTrackThumbnail, val width: Int, val height: Int) : DataFetcher { + override fun loadData(priority: Priority, callback: DataCallback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + callback.onLoadFailed(Exception("unsupported Android version")) + return + } + + val context = model.context + val uri = model.uri + val trackIndex = model.trackIndex + + val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) + if (imageIndex == null) { + callback.onLoadFailed(Exception("no image index")) + return + } + + val bitmap: Bitmap? + + val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return + try { + bitmap = retriever.getImageAtIndex(imageIndex) + } catch (e: Exception) { + callback.onLoadFailed(e) + return + } finally { + // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs + retriever.release() + } + + if (bitmap == null) { + callback.onLoadFailed(Exception("null bitmap")) + } else { + callback.onDataReady(bitmap.getBytes()?.inputStream()) + } + } + + private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? { + val extractor = MediaExtractor() + try { + extractor.setDataSource(context, uri, null) + val trackCount = extractor.trackCount + if (trackIndex < trackCount) { + var imageIndex = 0 + for (i in 0 until trackIndex) { + val mimeType = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME) + if (MimeTypes.isImage(mimeType)) imageIndex++ + } + return imageIndex + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e) + } finally { + extractor.release() + } + return null + } + + // already cleaned up in loadData and ByteArrayInputStream will be GC'd + override fun cleanup() {} + + // cannot cancel + override fun cancel() {} + + override fun getDataClass(): Class = InputStream::class.java + + override fun getDataSource(): DataSource = DataSource.LOCAL + + companion object { + private val LOG_TAG = LogUtils.createTag(MultiTrackThumbnailFetcher::class.java) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 9f7766d36..cb41fd458 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -9,7 +9,7 @@ object MimeTypes { private const val BMP = "image/bmp" const val GIF = "image/gif" const val HEIC = "image/heic" - const val HEIF = "image/heif" + private const val HEIF = "image/heif" private const val ICO = "image/x-icon" private const val JPEG = "image/jpeg" private const val PNG = "image/png" @@ -41,10 +41,9 @@ object MimeTypes { fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO) - fun isMultimedia(mimeType: String?) = when (mimeType) { - HEIC, HEIF -> true - else -> isVideo(mimeType) - } + fun isHeifLike(mimeType: String?) = mimeType != null && (mimeType == HEIC || mimeType == HEIF) + + fun isMultimedia(mimeType: String?) = isVideo(mimeType) || isHeifLike(mimeType) fun isRaw(mimeType: String): Boolean { return when (mimeType) { diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index ce08b6967..9170c8ec5 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -23,7 +23,7 @@ class RegionProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('uri=${key.uri}, regionRect=${key.regionRect}'); + yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, regionRect=${key.regionRect}'); }, ); } @@ -31,6 +31,7 @@ class RegionProvider extends ImageProvider { Future _loadAsync(RegionProviderKey key, DecoderCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; + final page = key.page; try { final bytes = await ImageFileService.getRegion( uri, @@ -40,7 +41,7 @@ class RegionProvider extends ImageProvider { key.sampleSize, key.regionRect, key.imageSize, - page: key.page, + page: page, taskKey: key, ); if (bytes == null) { @@ -49,7 +50,7 @@ class RegionProvider extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); - throw StateError('$mimeType region decoding failed'); + throw StateError('$mimeType region decoding failed (page $page)'); } } @@ -75,7 +76,7 @@ class RegionProviderKey { @required this.mimeType, @required this.rotationDegrees, @required this.isFlipped, - this.page = 0, + this.page, @required this.sampleSize, @required this.regionRect, @required this.imageSize, @@ -93,7 +94,6 @@ class RegionProviderKey { // but the entry attributes may change over time factory RegionProviderKey.fromEntry( ImageEntry entry, { - int page = 0, @required int sampleSize, @required Rectangle rect, }) { @@ -102,7 +102,7 @@ class RegionProviderKey { mimeType: entry.mimeType, rotationDegrees: entry.rotationDegrees, isFlipped: entry.isFlipped, - page: page, + page: entry.page, sampleSize: sampleSize, regionRect: rect, imageSize: Size(entry.width.toDouble(), entry.height.toDouble()), diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index 62546bbae..be69e5eca 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -24,7 +24,7 @@ class ThumbnailProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('uri=${key.uri}, extent=${key.extent}'); + yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, extent=${key.extent}'); }, ); } @@ -32,6 +32,7 @@ class ThumbnailProvider extends ImageProvider { Future _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; + final page = key.page; try { final bytes = await ImageFileService.getThumbnail( uri, @@ -41,7 +42,7 @@ class ThumbnailProvider extends ImageProvider { key.isFlipped, key.extent, key.extent, - page: key.page, + page: page, taskKey: key, ); if (bytes == null) { @@ -50,7 +51,7 @@ class ThumbnailProvider extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with uri=$uri, error=$error'); - throw StateError('$mimeType decoding failed'); + throw StateError('$mimeType decoding failed (page $page)'); } } @@ -75,7 +76,7 @@ class ThumbnailProviderKey { @required this.dateModifiedSecs, @required this.rotationDegrees, @required this.isFlipped, - this.page = 0, + this.page, this.extent = 0, this.scale = 1, }) : assert(uri != null), @@ -88,7 +89,7 @@ class ThumbnailProviderKey { // do not store the entry as it is, because the key should be constant // but the entry attributes may change over time - factory ThumbnailProviderKey.fromEntry(ImageEntry entry, {int page = 0, double extent = 0}) { + factory ThumbnailProviderKey.fromEntry(ImageEntry entry, {double extent = 0}) { return ThumbnailProviderKey( uri: entry.uri, mimeType: entry.mimeType, @@ -96,7 +97,7 @@ class ThumbnailProviderKey { dateModifiedSecs: entry.dateModifiedSecs ?? -1, rotationDegrees: entry.rotationDegrees, isFlipped: entry.isFlipped, - page: page, + page: entry.page, extent: extent, ); } diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index 5290913f9..22749c5a1 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -15,7 +15,7 @@ class UriImage extends ImageProvider { const UriImage({ @required this.uri, @required this.mimeType, - this.page = 0, + this.page, @required this.rotationDegrees, @required this.isFlipped, this.expectedContentLength, @@ -37,7 +37,7 @@ class UriImage extends ImageProvider { scale: key.scale, chunkEvents: chunkEvents.stream, informationCollector: () sync* { - yield ErrorDescription('uri=$uri, mimeType=$mimeType'); + yield ErrorDescription('uri=$uri, page=$page, mimeType=$mimeType'); }, ); } @@ -66,7 +66,7 @@ class UriImage extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); - throw StateError('$mimeType decoding failed'); + throw StateError('$mimeType decoding failed (page $page)'); } finally { unawaited(chunkEvents.close()); } diff --git a/lib/model/entry_cache.dart b/lib/model/entry_cache.dart index a940821e0..f135107e9 100644 --- a/lib/model/entry_cache.dart +++ b/lib/model/entry_cache.dart @@ -12,14 +12,12 @@ class EntryCache { int oldRotationDegrees, bool oldIsFlipped, ) async { - // TODO TLAD revisit this for multipage items, if someday image editing features are added for them - const page = 0; + // TODO TLAD provide page parameter for multipage items, if someday image editing features are added for them // evict fullscreen image await UriImage( uri: uri, mimeType: mimeType, - page: page, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, ).evict(); @@ -31,7 +29,6 @@ class EntryCache { dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, - page: page, )).evict(); // evict higher quality thumbnails (with powers of 2 from 32 to 1024 as specified extents) @@ -44,7 +41,6 @@ class EntryCache { dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, - page: page, extent: extent, )).evict()); } diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 1d1e6f697..a275e8108 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -24,7 +24,7 @@ import '../ref/mime_types.dart'; class ImageEntry { String uri; String _path, _directory, _filename, _extension; - int contentId; + int page, contentId; final String sourceMimeType; int width; int height; @@ -47,6 +47,7 @@ class ImageEntry { this.uri, String path, this.contentId, + this.page, this.sourceMimeType, @required this.width, @required this.height, @@ -93,6 +94,35 @@ class ImageEntry { return copied; } + ImageEntry getPageEntry({ + @required MultiPageInfo multiPageInfo, + @required int page, + }) { + final pageInfo = (multiPageInfo?.pages ?? {})[page]; + if (pageInfo == null) return this; + return AvesPageEntry( + pageInfo: pageInfo, + uri: uri, + path: path, + contentId: contentId, + page: page, + sourceMimeType: sourceMimeType, + width: width, + height: height, + sourceRotationDegrees: sourceRotationDegrees, + sizeBytes: sizeBytes, + sourceTitle: sourceTitle, + dateModifiedSecs: dateModifiedSecs, + sourceDateTakenMillis: sourceDateTakenMillis, + durationMillis: durationMillis, + ) + ..catalogMetadata = _catalogMetadata?.copyWith( + mimeType: pageInfo.mimeType, + isMultipage: false, + ) + ..addressDetails = _addressDetails?.copyWith(); + } + // from DB or platform source entry factory ImageEntry.fromMap(Map map) { return ImageEntry( @@ -243,18 +273,9 @@ class ImageEntry { static const ratioSeparator = '\u2236'; static const resolutionSeparator = ' \u00D7 '; - String getResolutionText({MultiPageInfo multiPageInfo, int page}) { - int w; - int h; - if (multiPageInfo != null && page != null) { - final pageInfo = multiPageInfo.pages[page]; - w = pageInfo?.width; - h = pageInfo?.height; - } - w ??= width; - h ??= height; - final ws = w ?? '?'; - final hs = h ?? '?'; + String get resolutionText { + final ws = width ?? '?'; + final hs = height ?? '?'; return isPortrait ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs'; } @@ -274,17 +295,10 @@ class ImageEntry { return isPortrait ? height / width : width / height; } - Size getDisplaySize({MultiPageInfo multiPageInfo, int page}) { - int w; - int h; - if (multiPageInfo != null && page != null) { - final pageInfo = multiPageInfo.pages[page]; - w = pageInfo?.width; - h = pageInfo?.height; - } - w ??= width; - h ??= height; - return isPortrait ? Size(h.toDouble(), w.toDouble()) : Size(w.toDouble(), h.toDouble()); + Size get displaySize { + final w = width.toDouble(); + final h = height.toDouble(); + return isPortrait ? Size(h, w) : Size(w, h); } int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null; diff --git a/lib/model/image_metadata.dart b/lib/model/image_metadata.dart index ccbbad3c0..e750fbbb0 100644 --- a/lib/model/image_metadata.dart +++ b/lib/model/image_metadata.dart @@ -68,17 +68,19 @@ class CatalogMetadata { } CatalogMetadata copyWith({ - @required int contentId, + int contentId, + String mimeType, + bool isMultipage, }) { return CatalogMetadata( contentId: contentId ?? this.contentId, - mimeType: mimeType, + mimeType: mimeType ?? this.mimeType, dateMillis: dateMillis, isAnimated: isAnimated, isFlipped: isFlipped, isGeotiff: isGeotiff, is360: is360, - isMultipage: isMultipage, + isMultipage: isMultipage ?? this.isMultipage, rotationDegrees: rotationDegrees, xmpSubjects: xmpSubjects, xmpTitleDescription: xmpTitleDescription, @@ -169,7 +171,7 @@ class AddressDetails { }); AddressDetails copyWith({ - @required int contentId, + int contentId, }) { return AddressDetails( contentId: contentId ?? this.contentId, diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index 7ca616792..45af27276 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -1,24 +1,6 @@ +import 'package:aves/model/image_entry.dart'; import 'package:flutter/foundation.dart'; -class SinglePageInfo { - final int width, height; - - SinglePageInfo({ - this.width, - this.height, - }); - - factory SinglePageInfo.fromMap(Map map) { - return SinglePageInfo( - width: map['width'] as int, - height: map['height'] as int, - ); - } - - @override - String toString() => '$runtimeType#${shortHash(this)}{width=$width, height=$height}'; -} - class MultiPageInfo { final Map pages; @@ -40,3 +22,65 @@ class MultiPageInfo { @override String toString() => '$runtimeType#${shortHash(this)}{pages=$pages}'; } + +class SinglePageInfo { + final String mimeType; + final int width, height; + final int trackId, durationMillis; + + SinglePageInfo({ + this.mimeType, + this.width, + this.height, + this.trackId, + this.durationMillis, + }); + + factory SinglePageInfo.fromMap(Map map) { + return SinglePageInfo( + mimeType: map['mimeType'] as String, + width: map['width'] as int, + height: map['height'] as int, + trackId: map['trackId'] as int, + durationMillis: map['durationMillis'] as int, + ); + } + + @override + String toString() => '$runtimeType#${shortHash(this)}{mimeType=$mimeType, width=$width, height=$height, trackId=$trackId, durationMillis=$durationMillis}'; +} + +class AvesPageEntry extends ImageEntry { + final SinglePageInfo pageInfo; + + AvesPageEntry({ + @required this.pageInfo, + String uri, + String path, + int contentId, + int page, + String sourceMimeType, + int width, + int height, + int sourceRotationDegrees, + int sizeBytes, + String sourceTitle, + int dateModifiedSecs, + int sourceDateTakenMillis, + int durationMillis, + }) : super( + uri: uri, + path: path, + contentId: contentId, + page: page, + sourceMimeType: pageInfo.mimeType ?? sourceMimeType, + width: pageInfo.width ?? width, + height: pageInfo.height ?? height, + sourceRotationDegrees: sourceRotationDegrees, + sizeBytes: sizeBytes, + sourceTitle: sourceTitle, + dateModifiedSecs: dateModifiedSecs, + sourceDateTakenMillis: sourceDateTakenMillis, + durationMillis: pageInfo.durationMillis ?? durationMillis, + ); +} diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 5ad41ffb2..d69cff77b 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -74,7 +74,7 @@ class ImageFileService { String mimeType, int rotationDegrees, bool isFlipped, { - int page = 0, + int page, int expectedContentLength, BytesReceivedCallback onBytesReceived, }) { @@ -87,7 +87,7 @@ class ImageFileService { 'mimeType': mimeType, 'rotationDegrees': rotationDegrees ?? 0, 'isFlipped': isFlipped ?? false, - 'page': page ?? 0, + 'page': page, }).listen( (data) { final chunk = data as Uint8List; @@ -125,7 +125,7 @@ class ImageFileService { int sampleSize, Rectangle regionRect, Size imageSize, { - int page = 0, + int page, Object taskKey, int priority, }) { diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 660a1159d..65c473554 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -19,7 +19,7 @@ class RasterImageThumbnail extends StatefulWidget { Key key, @required this.entry, @required this.extent, - this.page = 0, + this.page, this.isScrollingNotifier, this.heroTag, }) : super(key: key); @@ -33,8 +33,6 @@ class _RasterImageThumbnailState extends State { ImageEntry get entry => widget.entry; - int get page => widget.page; - double get extent => widget.extent; Object get heroTag => widget.heroTag; @@ -79,11 +77,11 @@ class _RasterImageThumbnailState extends State { if (!entry.canDecode) return; _fastThumbnailProvider = ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, page: page), + ThumbnailProviderKey.fromEntry(entry), ); if (!entry.isVideo) { _sizedThumbnailProvider = ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, page: page, extent: requestExtent), + ThumbnailProviderKey.fromEntry(entry, extent: requestExtent), ); } } @@ -153,7 +151,7 @@ class _RasterImageThumbnailState extends State { final imageProvider = UriImage( uri: entry.uri, mimeType: entry.mimeType, - page: page, + page: entry.page, rotationDegrees: entry.rotationDegrees, isFlipped: entry.isFlipped, expectedContentLength: entry.sizeBytes, diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index 54cb811b9..5561f587b 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -29,7 +29,7 @@ class VectorImageThumbnail extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { final availableSize = constraints.biggest; - final fitSize = applyBoxFit(fit, entry.getDisplaySize(), availableSize).destination; + final fitSize = applyBoxFit(fit, entry.displaySize, availableSize).destination; final offset = fitSize / 2 - availableSize / 2; final child = DecoratedBox( decoration: CheckeredDecoration(checkSize: extent / 8, offset: offset), diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index 21b2aa728..9fbe1403e 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -80,7 +80,7 @@ class ViewerDebugPage extends StatelessWidget { 'isFlipped': '${entry.isFlipped}', 'portrait': '${entry.isPortrait}', 'displayAspectRatio': '${entry.displayAspectRatio}', - 'displaySize': '${entry.getDisplaySize()}', + 'displaySize': '${entry.displaySize}', }), Divider(), InfoRowGroup({ diff --git a/lib/widgets/viewer/entry_scroller.dart b/lib/widgets/viewer/entry_scroller.dart index 58b799a9d..7f8e124b1 100644 --- a/lib/widgets/viewer/entry_scroller.dart +++ b/lib/widgets/viewer/entry_scroller.dart @@ -79,10 +79,10 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - EntryPageView _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page = 0}) { + EntryPageView _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page}) { return EntryPageView( key: Key('imageview'), - entry: entry, + mainEntry: entry, multiPageInfo: multiPageInfo, page: page, heroTag: widget.collection.heroTag(entry), @@ -150,9 +150,9 @@ class _SingleEntryScrollerState extends State with Automati ); } - EntryPageView _buildViewer({MultiPageInfo multiPageInfo, int page = 0}) { + EntryPageView _buildViewer({MultiPageInfo multiPageInfo, int page}) { return EntryPageView( - entry: entry, + mainEntry: entry, multiPageInfo: multiPageInfo, page: page, onTap: (_) => widget.onTap?.call(), diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index bab656c2b..db19c30c4 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -30,7 +30,7 @@ class BasicSection extends StatelessWidget { bool get showMegaPixels => entry.isPhoto && megaPixels != null && megaPixels > 0; - String get rasterResolutionText => '${entry.getResolutionText()}${showMegaPixels ? ' ($megaPixels MP)' : ''}'; + String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' ($megaPixels MP)' : ''}'; @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 9863c3793..d6ba71003 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -97,15 +97,32 @@ class _ViewerBottomOverlayState extends State { _lastDetails = snapshot.data; _lastEntry = entry; } - return _lastEntry == null - ? SizedBox.shrink() - : _BottomOverlayContent( - entry: _lastEntry, - details: _lastDetails, - position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null, - availableWidth: availableWidth, - multiPageController: multiPageController, - ); + if (_lastEntry == null) return SizedBox.shrink(); + + Widget _buildContent({MultiPageInfo multiPageInfo, int page}) => _BottomOverlayContent( + mainEntry: _lastEntry, + multiPageInfo: multiPageInfo, + page: page, + details: _lastDetails, + position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null, + availableWidth: availableWidth, + multiPageController: multiPageController, + ); + + if (multiPageController == null) return _buildContent(); + + return FutureBuilder( + future: multiPageController.info, + builder: (context, snapshot) { + final multiPageInfo = snapshot.data; + return ValueListenableBuilder( + valueListenable: multiPageController.pageNotifier, + builder: (context, page, child) { + return _buildContent(multiPageInfo: multiPageInfo, page: page); + }, + ); + }, + ); }, ), ); @@ -121,7 +138,9 @@ const double _interRowPadding = 2.0; const double _subRowMinWidth = 300.0; class _BottomOverlayContent extends AnimatedWidget { - final ImageEntry entry; + final ImageEntry mainEntry, entry; + final MultiPageInfo multiPageInfo; + final int page; final OverlayMetadata details; final String position; final double availableWidth; @@ -131,12 +150,15 @@ class _BottomOverlayContent extends AnimatedWidget { _BottomOverlayContent({ Key key, - this.entry, + this.mainEntry, + this.multiPageInfo, + this.page, this.details, this.position, this.availableWidth, this.multiPageController, - }) : super(key: key, listenable: entry.metadataChangeNotifier); + }) : entry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page), + super(key: key, listenable: mainEntry.metadataChangeNotifier); @override Widget build(BuildContext context) { @@ -158,13 +180,13 @@ class _BottomOverlayContent extends AnimatedWidget { infoColumn = _buildInfoColumn(orientation); } - if (multiPageController != null) { + if (mainEntry.isMultipage && multiPageController != null) { infoColumn = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ MultiPageOverlay( - entry: entry, + mainEntry: mainEntry, controller: multiPageController, availableWidth: availableWidth, ), @@ -340,12 +362,7 @@ class _PositionTitleRow extends StatelessWidget { // page count may be 0 when we know an entry to have multiple pages // but fail to get information about these pages final missingInfo = pageCount == 0; - return ValueListenableBuilder( - valueListenable: multiPageController.pageNotifier, - builder: (context, page, child) { - return toText(pagePosition: missingInfo ? null : '${page + 1}/${pageCount ?? '?'}'); - }, - ); + return toText(pagePosition: missingInfo ? null : '${(entry.page ?? 0) + 1}/${pageCount ?? '?'}'); }, ); } @@ -364,40 +381,14 @@ class _DateRow extends StatelessWidget { Widget build(BuildContext context) { final date = entry.bestDate; final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.overlayUnknown; + final resolutionText = entry.isSvg ? entry.aspectRatioText : entry.resolutionText; - Text toText({MultiPageInfo multiPageInfo, int page}) => Text( - entry.isSvg - ? entry.aspectRatioText - : entry.getResolutionText( - multiPageInfo: multiPageInfo, - page: page, - ), - strutStyle: Constants.overflowStrutStyle, - ); - - Widget resolutionText; - if (multiPageController != null) { - resolutionText = FutureBuilder( - future: multiPageController.info, - builder: (context, snapshot) { - final multiPageInfo = snapshot.data; - return ValueListenableBuilder( - valueListenable: multiPageController.pageNotifier, - builder: (context, page, child) { - return toText(multiPageInfo: multiPageInfo, page: page); - }, - ); - }, - ); - } else { - resolutionText = toText(); - } return Row( children: [ DecoratedIcon(AIcons.date, shadows: [Constants.embossShadow], size: _iconSize), SizedBox(width: _iconPadding), Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), - Expanded(flex: 2, child: resolutionText), + Expanded(flex: 2, child: Text(resolutionText, strutStyle: Constants.overflowStrutStyle)), ], ); } diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index ce9c6b101..0e1a3a808 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class Minimap extends StatelessWidget { - final ImageEntry entry; + final ImageEntry mainEntry; final ValueNotifier viewStateNotifier; final MultiPageController multiPageController; final Size size; @@ -16,7 +16,7 @@ class Minimap extends StatelessWidget { static const defaultSize = Size(96, 96); const Minimap({ - @required this.entry, + @required this.mainEntry, @required this.viewStateNotifier, @required this.multiPageController, this.size = defaultSize, @@ -34,11 +34,12 @@ class Minimap extends StatelessWidget { return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { - return _buildForEntrySize(entry.getDisplaySize(multiPageInfo: multiPageInfo, page: page)); + final pageEntry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page); + return _buildForEntrySize(pageEntry.displaySize); }, ); }) - : _buildForEntrySize(entry.getDisplaySize()), + : _buildForEntrySize(mainEntry.displaySize), ); } diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index a5569967d..a3ae822d6 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -3,22 +3,25 @@ import 'dart:math'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/collection/thumbnail/overlay.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class MultiPageOverlay extends StatefulWidget { - final ImageEntry entry; + final ImageEntry mainEntry; final MultiPageController controller; final double availableWidth; - const MultiPageOverlay({ + MultiPageOverlay({ Key key, - @required this.entry, + @required this.mainEntry, @required this.controller, @required this.availableWidth, - }) : super(key: key); + }) : assert(mainEntry.isMultipage), + assert(controller != null), + super(key: key); @override _MultiPageOverlayState createState() => _MultiPageOverlayState(); @@ -31,7 +34,7 @@ class _MultiPageOverlayState extends State { static const double extent = 48; static const double separatorWidth = 2; - ImageEntry get entry => widget.entry; + ImageEntry get mainEntry => widget.mainEntry; MultiPageController get controller => widget.controller; @@ -97,7 +100,7 @@ class _MultiPageOverlayState extends State { width: availableWidth, height: extent, child: ListView.separated( - key: ValueKey(entry), + key: ValueKey(mainEntry), scrollDirection: Axis.horizontal, controller: _scrollController, // default padding in scroll direction matches `MediaQuery.viewPadding`, @@ -106,6 +109,8 @@ class _MultiPageOverlayState extends State { itemBuilder: (context, index) { if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin; final page = index - 1; + final pageEntry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page); + return GestureDetector( onTap: () async { _syncScroll = false; @@ -117,15 +122,7 @@ class _MultiPageOverlayState extends State { ); _syncScroll = true; }, - child: Container( - width: extent, - height: extent, - child: RasterImageThumbnail( - entry: entry, - extent: extent, - page: page, - ), - ), + child: _buildPageThumbnail(pageEntry), ); }, separatorBuilder: (context, index) => separator, @@ -165,6 +162,35 @@ class _MultiPageOverlayState extends State { ); } + Widget _buildPageThumbnail(ImageEntry entry) { + Widget child = RasterImageThumbnail( + entry: entry, + extent: extent, + page: entry.page, + ); + + child = Stack( + alignment: Alignment.center, + children: [ + child, + Positioned( + bottom: 0, + left: 0, + child: ThumbnailEntryOverlay( + entry: entry, + extent: extent, + ), + ), + ], + ); + + return Container( + width: extent, + height: extent, + child: child, + ); + } + void _onScrollChange() { if (_syncScroll) { controller.page = scrollOffsetToPage(_scrollController.offset); diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index a3effb3cd..22815ea56 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -86,7 +86,7 @@ class ViewerTopOverlay extends StatelessWidget { FadeTransition( opacity: scale, child: Minimap( - entry: entry, + mainEntry: entry, viewStateNotifier: viewStateNotifier, multiPageController: multiPageController, ), diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 2eb5d03fb..3a1b267dc 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -16,12 +16,10 @@ class PanoramaPage extends StatefulWidget { static const routeName = '/viewer/panorama'; final ImageEntry entry; - final int page; final PanoramaInfo info; const PanoramaPage({ @required this.entry, - this.page = 0, @required this.info, }); @@ -77,7 +75,7 @@ class _PanoramaPageState extends State { image: UriImage( uri: entry.uri, mimeType: entry.mimeType, - page: widget.page, + page: entry.page, rotationDegrees: entry.rotationDegrees, isFlipped: entry.isFlipped, expectedContentLength: entry.sizeBytes, diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index 00824a099..f8f27fe6f 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -57,7 +57,7 @@ class EntryPrinter { return pages; } - Future _buildPageImage({page = 0}) async { + Future _buildPageImage({int page}) async { final uri = entry.uri; final mimeType = entry.mimeType; final rotationDegrees = entry.rotationDegrees; diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index ae4638de8..dc916af35 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -36,16 +36,17 @@ class EntryPageView extends StatefulWidget { static const minScale = ScaleLevel(ref: ScaleReference.contained); static const maxScale = ScaleLevel(factor: 2.0); - const EntryPageView({ + EntryPageView({ Key key, - @required this.entry, + ImageEntry mainEntry, this.multiPageInfo, - this.page = 0, + this.page, this.heroTag, @required this.onTap, @required this.videoControllers, this.onDisposed, - }) : super(key: key); + }) : entry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page) ?? mainEntry, + super(key: key); @override _EntryPageViewState createState() => _EntryPageViewState(); @@ -58,14 +59,8 @@ class _EntryPageViewState extends State { ImageEntry get entry => widget.entry; - MultiPageInfo get multiPageInfo => widget.multiPageInfo; - - int get page => widget.page; - MagnifierTapCallback get onTap => widget.onTap; - Size get pageDisplaySize => entry.getDisplaySize(multiPageInfo: multiPageInfo, page: page); - @override void initState() { super.initState(); @@ -86,7 +81,7 @@ class _EntryPageViewState extends State { Widget build(BuildContext context) { Widget child; if (entry.isVideo) { - if (entry.width > 0 && entry.height > 0) { + if (!entry.displaySize.isEmpty) { child = _buildVideoView(); } } else if (entry.isSvg) { @@ -109,15 +104,13 @@ class _EntryPageViewState extends State { Widget _buildRasterView() { return Magnifier( // key includes size and orientation to refresh when the image is rotated - key: ValueKey('${page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), + key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), child: TiledImageView( entry: entry, - multiPageInfo: multiPageInfo, - page: page, viewStateNotifier: _viewStateNotifier, errorBuilder: (context, error, stackTrace) => ErrorView(onTap: () => onTap?.call(null)), ), - childSize: pageDisplaySize, + childSize: entry.displaySize, controller: _magnifierController, maxScale: EntryPageView.maxScale, minScale: EntryPageView.minScale, @@ -139,7 +132,7 @@ class _EntryPageViewState extends State { colorFilter: colorFilter, ), ), - childSize: pageDisplaySize, + childSize: entry.displaySize, controller: _magnifierController, minScale: EntryPageView.minScale, initialScale: EntryPageView.initialScale, @@ -149,7 +142,7 @@ class _EntryPageViewState extends State { if (background == EntryBackground.checkered) { child = VectorViewCheckeredBackground( - displaySize: pageDisplaySize, + displaySize: entry.displaySize, viewStateNotifier: _viewStateNotifier, child: child, ); @@ -166,7 +159,7 @@ class _EntryPageViewState extends State { controller: videoController, ) : SizedBox(), - childSize: pageDisplaySize, + childSize: entry.displaySize, controller: _magnifierController, maxScale: EntryPageView.maxScale, minScale: EntryPageView.minScale, diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index cba13a263..9b0262e23 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -4,7 +4,6 @@ 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/image_entry.dart'; -import 'package:aves/model/multipage.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; @@ -17,15 +16,11 @@ import 'package:tuple/tuple.dart'; class TiledImageView extends StatefulWidget { final ImageEntry entry; - final MultiPageInfo multiPageInfo; - final int page; final ValueNotifier viewStateNotifier; final ImageErrorWidgetBuilder errorBuilder; const TiledImageView({ @required this.entry, - this.multiPageInfo, - this.page = 0, @required this.viewStateNotifier, @required this.errorBuilder, }); @@ -46,17 +41,15 @@ class _TiledImageViewState extends State { ImageEntry get entry => widget.entry; - int get page => widget.page; - ValueNotifier get viewStateNotifier => widget.viewStateNotifier; bool get useBackground => entry.canHaveAlpha && settings.rasterBackground != EntryBackground.transparent; // as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved // so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution - bool get useTiles => entry.canTile && (entry.getDisplaySize(multiPageInfo: widget.multiPageInfo, page: page).longestSide > 4096 || entry.is360); + bool get useTiles => entry.canTile && (entry.displaySize.longestSide > 4096 || entry.is360); - ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry, page: page)); + ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry)); ImageProvider get fullImageProvider { if (useTiles) { @@ -76,7 +69,6 @@ class _TiledImageViewState extends State { )?.item2; return RegionProvider(RegionProviderKey.fromEntry( entry, - page: page, sampleSize: _maxSampleSize, rect: regionRect, )); @@ -84,7 +76,7 @@ class _TiledImageViewState extends State { return UriImage( uri: entry.uri, mimeType: entry.mimeType, - page: page, + page: entry.page, rotationDegrees: entry.rotationDegrees, isFlipped: entry.isFlipped, expectedContentLength: entry.sizeBytes, @@ -98,7 +90,7 @@ class _TiledImageViewState extends State { @override void initState() { super.initState(); - _displaySize = entry.getDisplaySize(multiPageInfo: widget.multiPageInfo, page: page); + _displaySize = entry.displaySize; _fullImageListener = ImageStreamListener(_onFullImageCompleted); if (!useTiles) _registerFullImage(); } @@ -109,7 +101,7 @@ class _TiledImageViewState extends State { final oldViewState = oldWidget.viewStateNotifier.value; final viewState = widget.viewStateNotifier.value; - if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize || oldWidget.page != page) { + if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize) { _isTilingInitialized = false; _fullImageLoaded.value = false; _unregisterFullImage(); @@ -278,7 +270,6 @@ class _TiledImageViewState extends State { if (rects != null) { tiles.add(RegionTile( entry: entry, - page: page, tileRect: rects.item1, regionRect: rects.item2, sampleSize: sampleSize, @@ -347,7 +338,6 @@ class _TiledImageViewState extends State { class RegionTile extends StatefulWidget { final ImageEntry entry; - final int page; // `tileRect` uses Flutter view coordinates // `regionRect` uses the raw image pixel coordinates @@ -357,7 +347,6 @@ class RegionTile extends StatefulWidget { const RegionTile({ @required this.entry, - @required this.page, @required this.tileRect, @required this.regionRect, @required this.sampleSize, @@ -406,7 +395,6 @@ class _RegionTileState extends State { _provider = RegionProvider(RegionProviderKey.fromEntry( entry, - page: widget.page, sampleSize: widget.sampleSize, rect: widget.regionRect, )); diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 51c257072..66b67b1e2 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -101,7 +101,7 @@ class _AvesVideoState extends State { image: UriImage( uri: entry.uri, mimeType: entry.mimeType, - page: 0, + page: entry.page, rotationDegrees: entry.rotationDegrees, isFlipped: entry.isFlipped, expectedContentLength: entry.sizeBytes, From bd8bc19fa1940b28e9e825e1b06a5d8117039d3f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 19 Jan 2021 10:42:31 +0900 Subject: [PATCH 06/44] info: removed extra media image hints --- lib/widgets/viewer/info/metadata/metadata_dir_tile.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index 0ea17ab08..e84a655d6 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -61,13 +61,7 @@ class MetadataDirTile extends StatelessWidget { ); if (tags['Has Video'] == 'yes') prefixChildren.add(builder(AIcons.video)); if (tags['Has Audio'] == 'yes') prefixChildren.add(builder(AIcons.audio)); - if (tags['Has Image'] == 'yes') { - int count; - if (tags.containsKey('Image Count')) { - count = int.tryParse(tags['Image Count']); - } - prefixChildren.addAll(List.generate(count ?? 1, (i) => builder(AIcons.image))); - } + if (tags['Has Image'] == 'yes') prefixChildren.add(builder(AIcons.image)); break; } } From a37c10a96917ae96f3f1cdc2e99edbc7f86f1168 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 19 Jan 2021 12:19:38 +0900 Subject: [PATCH 07/44] temp: do not tile multi-track images --- android/app/build.gradle | 2 +- .../aves/channel/calls/ThumbnailFetcher.kt | 4 +- .../channel/streams/ImageByteStreamHandler.kt | 4 +- .../decoder/MultiTrackImageGlideModule.kt | 75 +++++++++++ .../decoder/MultiTrackThumbnailGlideModule.kt | 124 ------------------ .../aves/decoder/TiffThumbnailGlideModule.kt | 2 +- .../aves/decoder/VideoThumbnailGlideModule.kt | 2 +- .../thibault/aves/metadata/MultiTrackMedia.kt | 54 ++++++++ lib/model/image_entry.dart | 3 +- 9 files changed, 138 insertions(+), 132 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt delete mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackThumbnailGlideModule.kt create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index 7fd1f982e..47c92f2f2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' - implementation 'androidx.core:core-ktx:1.5.0-alpha05' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts + implementation 'androidx.core:core-ktx:1.5.0-beta01' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts implementation 'androidx.exifinterface:exifinterface:1.3.2' implementation 'com.commonsware.cwac:document:0.4.1' implementation 'com.drewnoakes:metadata-extractor:2.15.0' diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt index 57fa44102..d3c4d4605 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt @@ -13,7 +13,7 @@ import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.signature.ObjectKey -import deckers.thibault.aves.decoder.MultiTrackThumbnail +import deckers.thibault.aves.decoder.MultiTrackImage import deckers.thibault.aves.decoder.TiffThumbnail import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation @@ -128,7 +128,7 @@ class ThumbnailFetcher internal constructor( val model: Any = if (tiffFetch) { TiffThumbnail(context, uri, page ?: 0) } else if (multiTrackFetch) { - MultiTrackThumbnail(context, uri, page ?: 0) + MultiTrackImage(context, uri, page ?: 0) } else uri Glide.with(context) .asBitmap() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt index 1f3a155de..8a73fa1a2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -9,7 +9,7 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions -import deckers.thibault.aves.decoder.MultiTrackThumbnail +import deckers.thibault.aves.decoder.MultiTrackImage import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.getBytes @@ -118,7 +118,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen private fun streamImageByGlide(uri: Uri, page: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { val model: Any = if (isHeifLike(mimeType) && page != null) { - MultiTrackThumbnail(activity, uri, page) + MultiTrackImage(activity, uri, page) } else { uri } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt new file mode 100644 index 000000000..36b2d544a --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt @@ -0,0 +1,75 @@ +package deckers.thibault.aves.decoder + +import android.content.Context +import android.net.Uri +import android.os.Build +import com.bumptech.glide.Glide +import com.bumptech.glide.Priority +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.data.DataFetcher.DataCallback +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.module.LibraryGlideModule +import com.bumptech.glide.signature.ObjectKey +import deckers.thibault.aves.metadata.MultiTrackMedia +import deckers.thibault.aves.utils.BitmapUtils.getBytes +import java.io.InputStream + + +@GlideModule +class MultiTrackImageGlideModule : LibraryGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.append(MultiTrackImage::class.java, InputStream::class.java, MultiTrackThumbnailLoader.Factory()) + } +} + +class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int) + +internal class MultiTrackThumbnailLoader : ModelLoader { + override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData { + return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackImageFetcher(model, width, height)) + } + + override fun handles(model: MultiTrackImage): Boolean = true + + internal class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = MultiTrackThumbnailLoader() + + override fun teardown() {} + } +} + +internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int, val height: Int) : DataFetcher { + override fun loadData(priority: Priority, callback: DataCallback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + callback.onLoadFailed(Exception("unsupported Android version")) + return + } + + val context = model.context + val uri = model.uri + val trackIndex = model.trackIndex + + val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex) + if (bitmap == null) { + callback.onLoadFailed(Exception("null bitmap")) + } else { + callback.onDataReady(bitmap.getBytes()?.inputStream()) + } + } + + // already cleaned up in loadData and ByteArrayInputStream will be GC'd + override fun cleanup() {} + + // cannot cancel + override fun cancel() {} + + override fun getDataClass(): Class = InputStream::class.java + + override fun getDataSource(): DataSource = DataSource.LOCAL +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackThumbnailGlideModule.kt deleted file mode 100644 index 3d96d8b61..000000000 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackThumbnailGlideModule.kt +++ /dev/null @@ -1,124 +0,0 @@ -package deckers.thibault.aves.decoder - -import android.content.Context -import android.graphics.Bitmap -import android.media.MediaExtractor -import android.media.MediaFormat -import android.net.Uri -import android.os.Build -import android.util.Log -import com.bumptech.glide.Glide -import com.bumptech.glide.Priority -import com.bumptech.glide.Registry -import com.bumptech.glide.annotation.GlideModule -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.Options -import com.bumptech.glide.load.data.DataFetcher -import com.bumptech.glide.load.data.DataFetcher.DataCallback -import com.bumptech.glide.load.model.ModelLoader -import com.bumptech.glide.load.model.ModelLoaderFactory -import com.bumptech.glide.load.model.MultiModelLoaderFactory -import com.bumptech.glide.module.LibraryGlideModule -import com.bumptech.glide.signature.ObjectKey -import deckers.thibault.aves.utils.BitmapUtils.getBytes -import deckers.thibault.aves.utils.LogUtils -import deckers.thibault.aves.utils.MimeTypes -import deckers.thibault.aves.utils.StorageUtils -import java.io.InputStream - - -@GlideModule -class MultiTrackThumbnailGlideModule : LibraryGlideModule() { - override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - registry.append(MultiTrackThumbnail::class.java, InputStream::class.java, MultiTrackThumbnailLoader.Factory()) - } -} - -class MultiTrackThumbnail(val context: Context, val uri: Uri, val trackIndex: Int) - -internal class MultiTrackThumbnailLoader : ModelLoader { - override fun buildLoadData(model: MultiTrackThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData { - return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackThumbnailFetcher(model, width, height)) - } - - override fun handles(MultiTrackThumbnail: MultiTrackThumbnail): Boolean = true - - internal class Factory : ModelLoaderFactory { - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = MultiTrackThumbnailLoader() - - override fun teardown() {} - } -} - -internal class MultiTrackThumbnailFetcher(val model: MultiTrackThumbnail, val width: Int, val height: Int) : DataFetcher { - override fun loadData(priority: Priority, callback: DataCallback) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - callback.onLoadFailed(Exception("unsupported Android version")) - return - } - - val context = model.context - val uri = model.uri - val trackIndex = model.trackIndex - - val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) - if (imageIndex == null) { - callback.onLoadFailed(Exception("no image index")) - return - } - - val bitmap: Bitmap? - - val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return - try { - bitmap = retriever.getImageAtIndex(imageIndex) - } catch (e: Exception) { - callback.onLoadFailed(e) - return - } finally { - // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs - retriever.release() - } - - if (bitmap == null) { - callback.onLoadFailed(Exception("null bitmap")) - } else { - callback.onDataReady(bitmap.getBytes()?.inputStream()) - } - } - - private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? { - val extractor = MediaExtractor() - try { - extractor.setDataSource(context, uri, null) - val trackCount = extractor.trackCount - if (trackIndex < trackCount) { - var imageIndex = 0 - for (i in 0 until trackIndex) { - val mimeType = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME) - if (MimeTypes.isImage(mimeType)) imageIndex++ - } - return imageIndex - } - } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e) - } finally { - extractor.release() - } - return null - } - - // already cleaned up in loadData and ByteArrayInputStream will be GC'd - override fun cleanup() {} - - // cannot cancel - override fun cancel() {} - - override fun getDataClass(): Class = InputStream::class.java - - override fun getDataSource(): DataSource = DataSource.LOCAL - - companion object { - private val LOG_TAG = LogUtils.createTag(MultiTrackThumbnailFetcher::class.java) - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt index 30c3627c2..529547d1e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt @@ -33,7 +33,7 @@ internal class TiffThumbnailLoader : ModelLoader { return ModelLoader.LoadData(ObjectKey(model.uri), TiffThumbnailFetcher(model, width, height)) } - override fun handles(tiffThumbnail: TiffThumbnail): Boolean = true + override fun handles(model: TiffThumbnail): Boolean = true internal class Factory : ModelLoaderFactory { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = TiffThumbnailLoader() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt index a6045c773..7a39b7b0a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt @@ -34,7 +34,7 @@ internal class VideoThumbnailLoader : ModelLoader { return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model)) } - override fun handles(videoThumbnail: VideoThumbnail): Boolean = true + override fun handles(model: VideoThumbnail): Boolean = true internal class Factory : ModelLoaderFactory { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = VideoThumbnailLoader() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt new file mode 100644 index 000000000..afc7f4976 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt @@ -0,0 +1,54 @@ +package deckers.thibault.aves.metadata + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaExtractor +import android.media.MediaFormat +import android.net.Uri +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.StorageUtils + +object MultiTrackMedia { + private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java) + + @RequiresApi(Build.VERSION_CODES.P) + fun getImage(context: Context, uri: Uri, trackIndex: Int): Bitmap? { + val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) ?: return null + + val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null + try { + return retriever.getImageAtIndex(imageIndex) + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to extract image from uri=$uri trackIndex=$trackIndex imageIndex=$imageIndex", e) + } finally { + // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs + retriever.release() + } + return null + } + + private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? { + val extractor = MediaExtractor() + try { + extractor.setDataSource(context, uri, null) + val trackCount = extractor.trackCount + if (trackIndex < trackCount) { + var imageIndex = 0 + for (i in 0 until trackIndex) { + val mimeType = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME) + if (MimeTypes.isImage(mimeType)) imageIndex++ + } + return imageIndex + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e) + } finally { + extractor.release() + } + return null + } +} \ No newline at end of file diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index a275e8108..079c72782 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -224,7 +224,8 @@ class ImageEntry { MimeTypes.rw2, MimeTypes.srw, ].contains(mimeType) && - !isAnimated; + !isAnimated && + page == null; bool get canTile => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; From fd8191639b3a259fdc5d13bb79d9240a56d86373 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 19 Jan 2021 18:31:18 +0900 Subject: [PATCH 08/44] insets fixes --- lib/model/settings/map_style.dart | 34 +++++++++++++++ lib/model/settings/settings.dart | 2 +- lib/widgets/collection/collection_page.dart | 2 +- lib/widgets/collection/grid/selector.dart | 3 +- .../collection/thumbnail_collection.dart | 19 ++++----- .../insets.dart} | 19 +++++++-- lib/widgets/common/behaviour/routes.dart | 4 -- .../common/extensions/build_context.dart | 5 +++ .../common/extensions/media_query.dart | 41 +++++++++++++++++++ lib/widgets/debug/overlay.dart | 3 +- lib/widgets/drawer/app_drawer.dart | 4 +- lib/widgets/drawer/tile.dart | 2 +- .../filter_grids/common/filter_grid_page.dart | 28 +++++-------- lib/widgets/home_page.dart | 2 +- lib/widgets/viewer/entry_viewer_stack.dart | 2 +- lib/widgets/viewer/info/info_page.dart | 17 +++----- lib/widgets/viewer/info/location_section.dart | 36 +--------------- lib/widgets/viewer/info/maps/common.dart | 2 +- lib/widgets/viewer/info/maps/google_map.dart | 2 +- lib/widgets/viewer/info/maps/leaflet_map.dart | 3 +- lib/widgets/viewer/panorama_page.dart | 2 +- 21 files changed, 135 insertions(+), 97 deletions(-) create mode 100644 lib/model/settings/map_style.dart rename lib/widgets/common/{gesture_area_protector.dart => basic/insets.dart} (74%) create mode 100644 lib/widgets/common/extensions/build_context.dart create mode 100644 lib/widgets/common/extensions/media_query.dart diff --git a/lib/model/settings/map_style.dart b/lib/model/settings/map_style.dart new file mode 100644 index 000000000..25559107a --- /dev/null +++ b/lib/model/settings/map_style.dart @@ -0,0 +1,34 @@ +// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ +enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } + +extension ExtraEntryMapStyle on EntryMapStyle { + String get name { + switch (this) { + case EntryMapStyle.googleNormal: + return 'Google Maps'; + case EntryMapStyle.googleHybrid: + return 'Google Maps (Hybrid)'; + case EntryMapStyle.googleTerrain: + return 'Google Maps (Terrain)'; + case EntryMapStyle.osmHot: + return 'Humanitarian OSM'; + case EntryMapStyle.stamenToner: + return 'Stamen Toner'; + case EntryMapStyle.stamenWatercolor: + return 'Stamen Watercolor'; + default: + return toString(); + } + } + + bool get isGoogleMaps { + switch (this) { + case EntryMapStyle.googleNormal: + case EntryMapStyle.googleHybrid: + case EntryMapStyle.googleTerrain: + return true; + default: + return false; + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 3e5fe5246..fcc10a444 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -2,8 +2,8 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/home_page.dart'; +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/screen_on.dart'; -import 'package:aves/widgets/viewer/info/location_section.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index ba71b34c9..34fc10f68 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,7 +1,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/thumbnail_collection.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/widgets/collection/grid/selector.dart b/lib/widgets/collection/grid/selector.dart index 83c38e506..513a7beeb 100644 --- a/lib/widgets/collection/grid/selector.dart +++ b/lib/widgets/collection/grid/selector.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/math_utils.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -62,7 +63,7 @@ class _GridSelectionGestureDetectorState extends State().viewInsets.bottom, + bottom: context.read().effectiveBottomPadding, ); _scrollSpeedFactor = 0; _pressing = true; diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index cdc36a635..3ffed368c 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -16,8 +16,10 @@ import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/selector.dart'; import 'package:aves/widgets/collection/grid/thumbnail.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; -import 'package:aves/widgets/common/behaviour/routes.dart'; +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/sliver.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; @@ -222,14 +224,7 @@ class _CollectionScrollViewState extends State { hasScrollBody: false, ) : SectionedListSliver(), - SliverToBoxAdapter( - child: Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) { - return SizedBox(height: mqViewInsetsBottom); - }, - ), - ), + BottomPaddingSliver(), ], ); } @@ -238,8 +233,8 @@ class _CollectionScrollViewState extends State { return ValueListenableBuilder( valueListenable: widget.appBarHeightNotifier, builder: (context, appBarHeight, child) => Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) => DraggableScrollbar( + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) => DraggableScrollbar( heightScrollThumb: avesScrollThumbHeight, backgroundColor: Colors.white, scrollThumbBuilder: avesScrollThumbBuilder( @@ -250,7 +245,7 @@ class _CollectionScrollViewState extends State { padding: EdgeInsets.only( // padding to keep scroll thumb between app bar above and nav bar below top: appBarHeight, - bottom: mqViewInsetsBottom, + bottom: mqPaddingBottom, ), child: scrollView, ), diff --git a/lib/widgets/common/gesture_area_protector.dart b/lib/widgets/common/basic/insets.dart similarity index 74% rename from lib/widgets/common/gesture_area_protector.dart rename to lib/widgets/common/basic/insets.dart index 0fb879ded..50d2fd56c 100644 --- a/lib/widgets/common/gesture_area_protector.dart +++ b/lib/widgets/common/basic/insets.dart @@ -1,5 +1,4 @@ -import 'dart:math'; - +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -15,7 +14,7 @@ class BottomGestureAreaProtector extends StatelessWidget { @override Widget build(BuildContext context) { return Selector( - selector: (c, mq) => max(mq.viewPadding.bottom, mq.viewInsets.bottom), + selector: (c, mq) => mq.effectiveBottomPadding, builder: (c, mqPaddingBottom, child) { // devices with physical navigation buttons have no bottom insets // we assume these devices do not use gesture navigation @@ -47,3 +46,17 @@ class GestureAreaProtectorStack extends StatelessWidget { ); } } + +class BottomPaddingSliver extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Selector( + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) { + return SizedBox(height: mqPaddingBottom); + }, + ), + ); + } +} diff --git a/lib/widgets/common/behaviour/routes.dart b/lib/widgets/common/behaviour/routes.dart index 03e460480..a5ac9d079 100644 --- a/lib/widgets/common/behaviour/routes.dart +++ b/lib/widgets/common/behaviour/routes.dart @@ -1,9 +1,5 @@ import 'package:flutter/material.dart'; -extension ExtraContext on BuildContext { - String get currentRouteName => ModalRoute.of(this)?.settings?.name; -} - class DirectMaterialPageRoute extends PageRouteBuilder { DirectMaterialPageRoute({ RouteSettings settings, diff --git a/lib/widgets/common/extensions/build_context.dart b/lib/widgets/common/extensions/build_context.dart new file mode 100644 index 000000000..3f06767b8 --- /dev/null +++ b/lib/widgets/common/extensions/build_context.dart @@ -0,0 +1,5 @@ +import 'package:flutter/widgets.dart'; + +extension ExtraContext on BuildContext { + String get currentRouteName => ModalRoute.of(this)?.settings?.name; +} diff --git a/lib/widgets/common/extensions/media_query.dart b/lib/widgets/common/extensions/media_query.dart new file mode 100644 index 000000000..e0e15959c --- /dev/null +++ b/lib/widgets/common/extensions/media_query.dart @@ -0,0 +1,41 @@ +import 'dart:math'; + +import 'package:flutter/widgets.dart'; + +extension ExtraMediaQueryData on MediaQueryData { + /* + examples of MediaQuery props in practice, as of Flutter v1.22.5 + + S20, Android 11, portrait, notch top, button nav bar bottom + padding EdgeInsets(0.0, 26.0, 0.0, 48.0) + viewPadding EdgeInsets(0.0, 26.0, 0.0, 48.0) + viewInsets EdgeInsets.zero + + S20, Android 11, landscape, notch left, button nav bar right + padding EdgeInsets(26.0, 24.0, 0.0, 0.0) + viewPadding EdgeInsets(26.0, 24.0, 0.0, 0.0) + viewInsets EdgeInsets.zero + + S10e, Android 10, portrait, notch top, button nav bar bottom + padding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewPadding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewInsets EdgeInsets(0.0, 0.0, 0.0, 48.0) + + S10e, Android 10, portrait, notch top, gesture nav bar bottom + padding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewPadding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewInsets EdgeInsets(0.0, 0.0, 0.0, 15.0) + + S10e, Android 10, landscape, notch left, button nav bar right + padding EdgeInsets(38.7, 24.0, 0.0, 0.0) + viewPadding EdgeInsets(38.7, 24.0, 0.0, 0.0) + viewInsets EdgeInsets.zero + + S7, portrait/landscape, no notch, no nav bar + padding EdgeInsets(0.0, 24.0, 0.0, 0.0) + viewPadding EdgeInsets(0.0, 24.0, 0.0, 0.0) + viewInsets EdgeInsets.zero + */ + + double get effectiveBottomPadding => max(viewPadding.bottom, viewInsets.bottom); +} diff --git a/lib/widgets/debug/overlay.dart b/lib/widgets/debug/overlay.dart index 9c0081c0e..6fc169a14 100644 --- a/lib/widgets/debug/overlay.dart +++ b/lib/widgets/debug/overlay.dart @@ -1,4 +1,5 @@ import 'package:aves/services/service_policy.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:flutter/material.dart'; class DebugTaskQueueOverlay extends StatelessWidget { @@ -13,7 +14,7 @@ class DebugTaskQueueOverlay extends StatelessWidget { child: Container( color: Colors.indigo[900].withAlpha(0xCC), margin: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, + bottom: MediaQuery.of(context).effectiveBottomPadding, ), padding: EdgeInsets.all(8), child: StreamBuilder( diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index 22358d397..112fc28ae 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'dart:ui'; import 'package:aves/model/filters/album.dart'; @@ -12,6 +11,7 @@ import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/about/about_page.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; @@ -60,7 +60,7 @@ class _AppDrawerState extends State { return Drawer( child: Selector( - selector: (c, mq) => max(mq.viewPadding.bottom, mq.viewInsets.bottom), + selector: (c, mq) => mq.effectiveBottomPadding, builder: (c, mqPaddingBottom, child) { return SingleChildScrollView( padding: EdgeInsets.only(bottom: mqPaddingBottom), diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index b2bbb7614..be6b4f8b6 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 42387d473..97104b603 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -4,9 +4,10 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; -import 'package:aves/widgets/common/behaviour/routes.dart'; -import 'package:aves/widgets/common/gesture_area_protector.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/sliver.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -189,8 +190,8 @@ class FilterGridPage extends StatelessWidget { Widget _buildDraggableScrollView(ScrollView scrollView) { return Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) => DraggableScrollbar( + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) => DraggableScrollbar( heightScrollThumb: avesScrollThumbHeight, backgroundColor: Colors.white, scrollThumbBuilder: avesScrollThumbBuilder( @@ -201,7 +202,7 @@ class FilterGridPage extends StatelessWidget { padding: EdgeInsets.only( // padding to keep scroll thumb between app bar above and nav bar below top: _appBarHeightNotifier.value, - bottom: mqViewInsetsBottom, + bottom: mqPaddingBottom, ), child: scrollView, ), @@ -213,10 +214,10 @@ class FilterGridPage extends StatelessWidget { if (empty) { content = SliverFillRemaining( child: Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) { + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) { return Padding( - padding: EdgeInsets.only(bottom: mqViewInsetsBottom), + padding: EdgeInsets.only(bottom: mqPaddingBottom), child: emptyBuilder(), ); }, @@ -227,22 +228,13 @@ class FilterGridPage extends StatelessWidget { content = SectionedListSliver>(); } - final padding = SliverToBoxAdapter( - child: Selector( - selector: (context, mq) => mq.viewInsets.bottom, - builder: (context, mqViewInsetsBottom, child) { - return SizedBox(height: mqViewInsetsBottom); - }, - ), - ); - return CustomScrollView( key: _scrollableKey, controller: PrimaryScrollController.of(context), slivers: [ appBar, content, - padding, + BottomPaddingSliver(), ], ); } diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index e7ba1d8ef..d75f9c6dd 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -12,9 +12,9 @@ import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; -import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_page.dart'; +import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 116befdc2..584f3d6c4 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -8,7 +8,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; import 'package:aves/widgets/viewer/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/entry_scroller.dart'; diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 14364b425..fae51760e 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -2,7 +2,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/info/basic_section.dart'; import 'package:aves/widgets/viewer/info/info_app_bar.dart'; @@ -11,7 +11,6 @@ import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tuple/tuple.dart'; class InfoPage extends StatefulWidget { final CollectionLens collection; @@ -46,11 +45,9 @@ class _InfoPageState extends State { bottom: false, child: NotificationListener( onNotification: _handleTopScroll, - child: Selector>( - selector: (c, mq) => Tuple2(mq.size.width, mq.viewInsets.bottom), - builder: (c, mq, child) { - final mqWidth = mq.item1; - final mqViewInsetsBottom = mq.item2; + child: Selector( + selector: (c, mq) => mq.size.width, + builder: (c, mqWidth, child) { return ValueListenableBuilder( valueListenable: widget.entryNotifier, builder: (context, entry, child) { @@ -61,7 +58,6 @@ class _InfoPageState extends State { visibleNotifier: widget.visibleNotifier, scrollController: _scrollController, split: mqWidth > 400, - mqViewInsetsBottom: mqViewInsetsBottom, goToViewer: _goToViewer, ) : SizedBox.shrink(); @@ -115,7 +111,6 @@ class _InfoPageContent extends StatefulWidget { final ValueNotifier visibleNotifier; final ScrollController scrollController; final bool split; - final double mqViewInsetsBottom; final VoidCallback goToViewer; const _InfoPageContent({ @@ -125,7 +120,6 @@ class _InfoPageContent extends StatefulWidget { @required this.visibleNotifier, @required this.scrollController, @required this.split, - @required this.mqViewInsetsBottom, @required this.goToViewer, }) : super(key: key); @@ -190,9 +184,10 @@ class _InfoPageContentState extends State<_InfoPageContent> { sliver: basicAndLocationSliver, ), SliverPadding( - padding: horizontalPadding + EdgeInsets.only(bottom: 8 + widget.mqViewInsetsBottom), + padding: horizontalPadding + EdgeInsets.only(bottom: 8), sliver: metadataSliver, ), + BottomPaddingSliver(), ], ); } diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 3341d3d54..128bdf262 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/filters/location.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; @@ -190,38 +191,3 @@ class _AddressInfoGroupState extends State<_AddressInfoGroup> { ); } } - -// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ -enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } - -extension ExtraEntryMapStyle on EntryMapStyle { - String get name { - switch (this) { - case EntryMapStyle.googleNormal: - return 'Google Maps'; - case EntryMapStyle.googleHybrid: - return 'Google Maps (Hybrid)'; - case EntryMapStyle.googleTerrain: - return 'Google Maps (Terrain)'; - case EntryMapStyle.osmHot: - return 'Humanitarian OSM'; - case EntryMapStyle.stamenToner: - return 'Stamen Toner'; - case EntryMapStyle.stamenWatercolor: - return 'Stamen Watercolor'; - default: - return toString(); - } - } - - bool get isGoogleMaps { - switch (this) { - case EntryMapStyle.googleNormal: - case EntryMapStyle.googleHybrid: - case EntryMapStyle.googleTerrain: - return true; - default: - return false; - } - } -} diff --git a/lib/widgets/viewer/info/maps/common.dart b/lib/widgets/viewer/info/maps/common.dart index fb8fc89c1..d62506e1a 100644 --- a/lib/widgets/viewer/info/maps/common.dart +++ b/lib/widgets/viewer/info/maps/common.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; @@ -6,7 +7,6 @@ import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/viewer/info/location_section.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; diff --git a/lib/widgets/viewer/info/maps/google_map.dart b/lib/widgets/viewer/info/maps/google_map.dart index 0c6e05bd7..25781b1d1 100644 --- a/lib/widgets/viewer/info/maps/google_map.dart +++ b/lib/widgets/viewer/info/maps/google_map.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/viewer/info/location_section.dart'; import 'package:aves/widgets/viewer/info/maps/common.dart'; import 'package:aves/widgets/viewer/info/maps/marker.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/viewer/info/maps/leaflet_map.dart b/lib/widgets/viewer/info/maps/leaflet_map.dart index 02f53dbd8..d5d2ecaf6 100644 --- a/lib/widgets/viewer/info/maps/leaflet_map.dart +++ b/lib/widgets/viewer/info/maps/leaflet_map.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/viewer/info/maps/common.dart'; import 'package:aves/widgets/viewer/info/maps/scale_layer.dart'; @@ -7,8 +8,6 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:latlong/latlong.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../location_section.dart'; - class EntryLeafletMap extends StatefulWidget { final LatLng latLng; final String geoUri; diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 3a1b267dc..eec842376 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -2,7 +2,7 @@ import 'package:aves/image_providers/uri_image_provider.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/gesture_area_protector.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/foundation.dart'; From 4d8e2d4123aa33a81cad95c5b8adebd6f92fc658 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 19 Jan 2021 18:58:01 +0900 Subject: [PATCH 09/44] multipage: reset magnifier state when page dimensions change --- .../viewer/visual/entry_page_view.dart | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index dc916af35..bd36e6722 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -53,7 +53,7 @@ class EntryPageView extends StatefulWidget { } class _EntryPageViewState extends State { - final MagnifierController _magnifierController = MagnifierController(); + MagnifierController _magnifierController; final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); final List _subscriptions = []; @@ -64,17 +64,39 @@ class _EntryPageViewState extends State { @override void initState() { super.initState(); - _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); - _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); + _registerWidget(); + } + + @override + void didUpdateWidget(covariant EntryPageView oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.entry.displaySize != entry.displaySize) { + // do not reset the magnifier view state unless page dimensions change, + // in effect locking the zoom & position when browsing entry pages of the same size + _unregisterWidget(); + _registerWidget(); + } } @override void dispose() { + _unregisterWidget(); + widget.onDisposed?.call(); + super.dispose(); + } + + void _registerWidget() { + _viewStateNotifier.value = ViewState.zero; + _magnifierController = MagnifierController(); + _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); + _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); + } + + void _unregisterWidget() { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); - widget.onDisposed?.call(); - super.dispose(); } @override From c840190aecca6baab77c21a700306d0e69a80572 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 19 Jan 2021 19:06:03 +0900 Subject: [PATCH 10/44] collection: scroll to top on filter change --- lib/widgets/collection/thumbnail_collection.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index 3ffed368c..ccc9791dc 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -195,10 +195,12 @@ class _CollectionScrollViewState extends State { } void _registerWidget(CollectionScrollView widget) { + widget.collection.filterChangeNotifier.addListener(_onFilterChange); widget.scrollController.addListener(_onScrollChange); } void _unregisterWidget(CollectionScrollView widget) { + widget.collection.filterChangeNotifier.removeListener(_onFilterChange); widget.scrollController.removeListener(_onScrollChange); } @@ -281,6 +283,8 @@ class _CollectionScrollViewState extends State { ); } + void _onFilterChange() => widget.scrollController.jumpTo(0); + void _onScrollChange() { widget.isScrollingNotifier.value = true; _stopScrollMonitoringTimer(); From 695f8de2a4ac69d68f57e95654a09dec20f5d5ce Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 20 Jan 2021 10:55:00 +0900 Subject: [PATCH 11/44] do not show scrollbar thumb for non scrollable views --- lib/utils/constants.dart | 6 - .../collection/thumbnail_collection.dart | 4 +- .../common/basic/draggable_scrollbar.dart | 388 ++++++++++++++++++ lib/widgets/common/identity/scroll_thumb.dart | 3 +- lib/widgets/common/magnifier/magnifier.dart | 30 +- .../filter_grids/common/filter_grid_page.dart | 4 +- pubspec.lock | 9 - pubspec.yaml | 4 - 8 files changed, 409 insertions(+), 39 deletions(-) create mode 100644 lib/widgets/common/basic/draggable_scrollbar.dart diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index b31208632..51597f019 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -89,12 +89,6 @@ class Constants { licenseUrl: 'https://github.com/benPesso/flutter_decorated_icon/blob/master/LICENSE', sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon', ), - Dependency( - name: 'Draggable Scrollbar', - license: 'MIT', - licenseUrl: 'https://github.com/fluttercommunity/flutter-draggable-scrollbar/blob/master/LICENSE', - sourceUrl: 'https://github.com/fluttercommunity/flutter-draggable-scrollbar', - ), Dependency( name: 'Event Bus', license: 'MIT', diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index ccc9791dc..d9a123b67 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -16,6 +16,7 @@ import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/selector.dart'; import 'package:aves/widgets/collection/grid/thumbnail.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; 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'; @@ -26,7 +27,6 @@ import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/tile_extent_manager.dart'; -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -237,8 +237,8 @@ class _CollectionScrollViewState extends State { builder: (context, appBarHeight, child) => Selector( selector: (context, mq) => mq.effectiveBottomPadding, builder: (context, mqPaddingBottom, child) => DraggableScrollbar( - heightScrollThumb: avesScrollThumbHeight, backgroundColor: Colors.white, + scrollThumbHeight: avesScrollThumbHeight, scrollThumbBuilder: avesScrollThumbBuilder( height: avesScrollThumbHeight, backgroundColor: Colors.white, diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart new file mode 100644 index 000000000..c2e8d3292 --- /dev/null +++ b/lib/widgets/common/basic/draggable_scrollbar.dart @@ -0,0 +1,388 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/* + This is derived from `draggable_scrollbar` package v0.0.4: + - removed default thumb builders + - allow any `ScrollView` as child + - allow any `Widget` as label content + - moved out constraints responsibility + - various extent & thumb positioning fixes + */ + +/// Build the Scroll Thumb and label using the current configuration +typedef ScrollThumbBuilder = Widget Function( + Color backgroundColor, + Animation thumbAnimation, + Animation labelAnimation, + double height, { + Widget labelText, +}); + +/// Build a Text widget using the current scroll offset +typedef LabelTextBuilder = Widget Function(double offsetY); + +/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged +/// for quick navigation of the BoxScrollView. +class DraggableScrollbar extends StatefulWidget { + /// The background color of the label and thumb + final Color backgroundColor; + + /// The height of the scroll thumb + final double scrollThumbHeight; + + /// A function that builds a thumb using the current configuration + final ScrollThumbBuilder scrollThumbBuilder; + + /// The amount of padding that should surround the thumb + final EdgeInsetsGeometry padding; + + /// Determines how quickly the scrollbar will animate in and out + final Duration scrollbarAnimationDuration; + + /// How long should the thumb be visible before fading out + final Duration scrollbarTimeToFade; + + /// Build a Text widget from the current offset in the BoxScrollView + final LabelTextBuilder labelTextBuilder; + + /// The ScrollController for the BoxScrollView + final ScrollController controller; + + /// The view that will be scrolled with the scroll thumb + final ScrollView child; + + DraggableScrollbar({ + 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: 600), + this.labelTextBuilder, + @required this.child, + }) : assert(controller != null), + assert(scrollThumbBuilder != null), + 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, + }) { + final scrollThumbAndLabel = labelText == null + ? scrollThumb + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ScrollLabel( + animation: labelAnimation, + child: labelText, + backgroundColor: backgroundColor, + ), + scrollThumb, + ], + ); + return SlideFadeTransition( + animation: thumbAnimation, + child: scrollThumbAndLabel, + ); + } +} + +class ScrollLabel extends StatelessWidget { + final Animation animation; + final Color backgroundColor; + final Widget child; + + const ScrollLabel({ + Key key, + @required this.child, + @required this.animation, + @required this.backgroundColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: animation, + child: Container( + margin: EdgeInsets.only(right: 12.0), + child: Material( + elevation: 4.0, + color: backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(16.0)), + child: child, + ), + ), + ); + } +} + +class _DraggableScrollbarState extends State with TickerProviderStateMixin { + final ValueNotifier _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0); + bool _isDragInProcess = false; + + AnimationController _thumbAnimationController; + Animation _thumbAnimation; + AnimationController _labelAnimationController; + Animation _labelAnimation; + Timer _fadeoutTimer; + + @override + void initState() { + super.initState(); + + _thumbAnimationController = AnimationController( + vsync: this, + duration: widget.scrollbarAnimationDuration, + ); + + _thumbAnimation = CurvedAnimation( + parent: _thumbAnimationController, + curve: Curves.fastOutSlowIn, + ); + + _labelAnimationController = AnimationController( + vsync: this, + duration: widget.scrollbarAnimationDuration, + ); + + _labelAnimation = CurvedAnimation( + parent: _labelAnimationController, + curve: Curves.fastOutSlowIn, + ); + } + + @override + void dispose() { + _thumbAnimationController.dispose(); + _labelAnimationController.dispose(); + _fadeoutTimer?.cancel(); + super.dispose(); + } + + ScrollController get controller => widget.controller; + + double get thumbMaxScrollExtent => context.size.height - widget.scrollThumbHeight - (widget.padding?.vertical ?? 0.0); + + double get thumbMinScrollExtent => 0.0; + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + _onScrollNotification(notification); + return false; + }, + child: Stack( + children: [ + RepaintBoundary( + child: widget.child, + ), + RepaintBoundary( + child: GestureDetector( + onVerticalDragStart: _onVerticalDragStart, + onVerticalDragUpdate: _onVerticalDragUpdate, + onVerticalDragEnd: _onVerticalDragEnd, + child: ValueListenableBuilder( + valueListenable: _thumbOffsetNotifier, + builder: (context, thumbOffset, child) => Container( + alignment: AlignmentDirectional.topEnd, + padding: EdgeInsets.only(top: thumbOffset) + widget.padding, + child: widget.scrollThumbBuilder( + widget.backgroundColor, + _thumbAnimation, + _labelAnimation, + widget.scrollThumbHeight, + labelText: (widget.labelTextBuilder != null && _isDragInProcess) + ? ValueListenableBuilder( + valueListenable: _viewOffsetNotifier, + builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset), + ) + : null, + ), + ), + ), + ), + ), + ], + ), + ); + } + + void _onScrollNotification(ScrollNotification notification) { + final scrollMetrics = notification.metrics; + + // do not update the thumb if we cannot actually scroll + if (scrollMetrics.minScrollExtent >= scrollMetrics.maxScrollExtent) return; + + _viewOffsetNotifier.value = scrollMetrics.pixels; + + // we update the thumb position from the scrolled offset + // when the user is not dragging the thumb + if (!_isDragInProcess) { + if (notification is ScrollUpdateNotification) { + _thumbOffsetNotifier.value = (scrollMetrics.pixels / scrollMetrics.maxScrollExtent * thumbMaxScrollExtent).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); + } + + if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { + _showThumb(); + _scheduleFadeout(); + } + } + } + + void _onVerticalDragStart(DragStartDetails details) { + _labelAnimationController.forward(); + _fadeoutTimer?.cancel(); + setState(() => _isDragInProcess = true); + } + + void _onVerticalDragUpdate(DragUpdateDetails details) { + _showThumb(); + if (_isDragInProcess) { + // thumb offset + _thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + details.delta.dy).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); + + // scroll offset + final min = controller.position.minScrollExtent; + final max = controller.position.maxScrollExtent; + controller.jumpTo((_thumbOffsetNotifier.value / thumbMaxScrollExtent * max).clamp(min, max)); + } + } + + void _onVerticalDragEnd(DragEndDetails details) { + _scheduleFadeout(); + setState(() => _isDragInProcess = false); + } + + void _showThumb() { + if (_thumbAnimationController.status != AnimationStatus.forward) { + _thumbAnimationController.forward(); + } + } + + void _scheduleFadeout() { + _fadeoutTimer?.cancel(); + _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { + _thumbAnimationController.reverse(); + _labelAnimationController.reverse(); + _fadeoutTimer = null; + }); + } +} + +/// Draws 2 triangles like arrow up and arrow down +class ArrowCustomPainter extends CustomPainter { + Color color; + + ArrowCustomPainter(this.color); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + const width = 12.0; + const height = 8.0; + final baseX = size.width / 2; + final baseY = size.height / 2; + + canvas.drawPath( + _trianglePath(Offset(baseX, baseY - 2.0), width, height, true), + paint, + ); + canvas.drawPath( + _trianglePath(Offset(baseX, baseY + 2.0), width, height, false), + paint, + ); + } + + static Path _trianglePath(Offset o, double width, double height, bool isUp) { + return Path() + ..moveTo(o.dx, o.dy) + ..lineTo(o.dx + width, o.dy) + ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) + ..close(); + } +} + +///This cut 2 lines in arrow shape +class ArrowClipper extends CustomClipper { + @override + Path getClip(Size size) { + final path = Path(); + path.lineTo(0.0, size.height); + path.lineTo(size.width, size.height); + path.lineTo(size.width, 0.0); + path.lineTo(0.0, 0.0); + path.close(); + + const arrowWidth = 8.0; + final startPointX = (size.width - arrowWidth) / 2; + var startPointY = size.height / 2 - arrowWidth / 2; + path.moveTo(startPointX, startPointY); + path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); + path.lineTo(startPointX + arrowWidth, startPointY); + path.lineTo(startPointX + arrowWidth, startPointY + 1.0); + path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); + path.lineTo(startPointX, startPointY + 1.0); + path.close(); + + startPointY = size.height / 2 + arrowWidth / 2; + path.moveTo(startPointX + arrowWidth, startPointY); + path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); + path.lineTo(startPointX, startPointY); + path.lineTo(startPointX, startPointY - 1.0); + path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); + path.lineTo(startPointX + arrowWidth, startPointY - 1.0); + path.close(); + + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} + +class SlideFadeTransition extends StatelessWidget { + final Animation animation; + final Widget child; + + const SlideFadeTransition({ + 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, + child: SlideTransition( + position: Tween( + begin: Offset(0.3, 0.0), + end: Offset(0.0, 0.0), + ).animate(animation), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + ); + } +} diff --git a/lib/widgets/common/identity/scroll_thumb.dart b/lib/widgets/common/identity/scroll_thumb.dart index de907a8b3..268874983 100644 --- a/lib/widgets/common/identity/scroll_thumb.dart +++ b/lib/widgets/common/identity/scroll_thumb.dart @@ -1,4 +1,4 @@ -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; const double avesScrollThumbHeight = 48; @@ -35,7 +35,6 @@ ScrollThumbBuilder avesScrollThumbBuilder({ thumbAnimation: thumbAnimation, labelAnimation: labelAnimation, labelText: labelText, - alwaysVisibleScrollThumb: false, ); }; } diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index e73bebbde..22c7d83d6 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -6,16 +6,18 @@ import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:aves/widgets/common/magnifier/scale/state.dart'; import 'package:flutter/material.dart'; -/// `Magnifier` is derived from `photo_view` package v0.9.2: -/// - removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`) -/// - removed rotation and many customization parameters -/// - removed ignorable/ignoring partial notifiers -/// - formatted, renamed and reorganized -/// - fixed gesture recognizers when used inside a scrollable widget like `PageView` -/// - fixed corner hit detection when in containers scrollable in both axes -/// - fixed corner hit detection issues due to imprecise double comparisons -/// - added single & double tap position feedback -/// - fixed focus when scaling by double-tap/pinch +/* + `Magnifier` is derived from `photo_view` package v0.9.2: + - removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`) + - removed rotation and many customization parameters + - removed ignorable/ignoring partial notifiers + - formatted, renamed and reorganized + - fixed gesture recognizers when used inside a scrollable widget like `PageView` + - fixed corner hit detection when in containers scrollable in both axes + - fixed corner hit detection issues due to imprecise double comparisons + - added single & double tap position feedback + - fixed focus when scaling by double-tap/pinch + */ class Magnifier extends StatefulWidget { const Magnifier({ Key key, @@ -33,16 +35,16 @@ class Magnifier extends StatefulWidget { final Widget child; - /// The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. + // The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. final Size childSize; - /// Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. + // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. final ScaleLevel maxScale; - /// Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. + // Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. final ScaleLevel minScale; - /// Defines the size the image will assume when the component is initialized, it is proportional to the original image size. + // Defines the size the image will assume when the component is initialized, it is proportional to the original image size. final ScaleLevel initialScale; final MagnifierController controller; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 97104b603..4d866db2b 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -4,6 +4,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -20,7 +21,6 @@ import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:aves/widgets/filter_grids/common/section_layout.dart'; -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -192,8 +192,8 @@ class FilterGridPage extends StatelessWidget { return Selector( selector: (context, mq) => mq.effectiveBottomPadding, builder: (context, mqPaddingBottom, child) => DraggableScrollbar( - heightScrollThumb: avesScrollThumbHeight, backgroundColor: Colors.white, + scrollThumbHeight: avesScrollThumbHeight, scrollThumbBuilder: avesScrollThumbBuilder( height: avesScrollThumbHeight, backgroundColor: Colors.white, diff --git a/pubspec.lock b/pubspec.lock index fdff631b9..edd985531 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -148,15 +148,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" - draggable_scrollbar: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "3b823ae0a9def4edec62771f18e6348312bfce15" - url: "git://github.com/deckerst/flutter-draggable-scrollbar.git" - source: git - version: "0.0.4" event_bus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 60130769b..63fee4179 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,10 +34,6 @@ dependencies: charts_flutter: collection: decorated_icon: - draggable_scrollbar: -# path: ../flutter-draggable-scrollbar - git: - url: git://github.com/deckerst/flutter-draggable-scrollbar.git event_bus: expansion_tile_card: # path: ../expansion_tile_card From 1a50fcc65ee933bd03c96e8ded9417908212c891 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 20 Jan 2021 11:46:42 +0900 Subject: [PATCH 12/44] fixed max scroll offset for sectioned lists with spacing --- lib/widgets/common/grid/sliver.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart index d5effa5f8..4c74a1688 100644 --- a/lib/widgets/common/grid/sliver.dart +++ b/lib/widgets/common/grid/sliver.dart @@ -215,7 +215,7 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { if (child == null) { // We have run out of children. final layout = sectionAtIndex(index) ?? sectionLayouts.last; - estimatedMaxScrollOffset = layout.indexToLayoutOffset(index); + estimatedMaxScrollOffset = layout.maxOffset; break; } } else { From c9fa903309ce82d8b551998095865cd7d9c075a3 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 21 Jan 2021 11:46:33 +0900 Subject: [PATCH 13/44] thumbnail & transition image loading review --- .../app_icon_image_provider.dart | 2 +- lib/image_providers/region_provider.dart | 46 ++--- lib/image_providers/thumbnail_provider.dart | 50 ++---- lib/image_providers/uri_image_provider.dart | 4 +- lib/image_providers/uri_picture_provider.dart | 4 +- lib/main.dart | 4 +- lib/model/entry_cache.dart | 4 + lib/model/entry_images.dart | 63 +++++++ lib/model/image_entry.dart | 8 +- lib/services/app_shortcut_service.dart | 20 +-- lib/services/image_file_service.dart | 38 +++-- lib/services/svg_metadata_service.dart | 4 +- lib/widgets/collection/filter_bar.dart | 2 +- lib/widgets/collection/thumbnail/raster.dart | 33 +--- .../collection/thumbnail_collection.dart | 2 +- lib/widgets/common/magnifier/core/core.dart | 2 - lib/widgets/common/magnifier/magnifier.dart | 110 ++++-------- .../magnifier/scale/scale_boundaries.dart | 18 +- .../filter_grids/common/filter_grid_page.dart | 2 +- lib/widgets/viewer/debug_page.dart | 10 +- lib/widgets/viewer/entry_action_delegate.dart | 2 +- lib/widgets/viewer/entry_scroller.dart | 47 +++-- lib/widgets/viewer/info/maps/marker.dart | 2 +- lib/widgets/viewer/overlay/video.dart | 2 +- lib/widgets/viewer/panorama_page.dart | 11 +- lib/widgets/viewer/printer.dart | 26 +-- .../viewer/visual/entry_page_view.dart | 89 ++++++---- lib/widgets/viewer/visual/raster.dart | 160 ++++++++---------- lib/widgets/viewer/visual/video.dart | 28 ++- 29 files changed, 385 insertions(+), 408 deletions(-) create mode 100644 lib/model/entry_images.dart diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart index 06d21092c..cbc7490e5 100644 --- a/lib/image_providers/app_icon_image_provider.dart +++ b/lib/image_providers/app_icon_image_provider.dart @@ -2,7 +2,7 @@ import 'dart:ui' as ui show Codec; import 'package:aves/services/android_app_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class AppIconImage extends ImageProvider { const AppIconImage({ diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index 9170c8ec5..5d41855c4 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui show Codec; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class RegionProvider extends ImageProvider { final RegionProviderKey key; @@ -23,7 +22,7 @@ class RegionProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, regionRect=${key.regionRect}'); + yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, region=${key.region}'); }, ); } @@ -39,7 +38,7 @@ class RegionProvider extends ImageProvider { key.rotationDegrees, key.isFlipped, key.sampleSize, - key.regionRect, + key.region, key.imageSize, page: page, taskKey: key, @@ -64,21 +63,23 @@ class RegionProvider extends ImageProvider { } 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 rotationDegrees, sampleSize, page; + final int page, rotationDegrees, sampleSize; final bool isFlipped; - final Rectangle regionRect; + final Rectangle region; final Size imageSize; final double scale; const RegionProviderKey({ @required this.uri, @required this.mimeType, + @required this.page, @required this.rotationDegrees, @required this.isFlipped, - this.page, @required this.sampleSize, - @required this.regionRect, + @required this.region, @required this.imageSize, this.scale = 1.0, }) : assert(uri != null), @@ -86,48 +87,29 @@ class RegionProviderKey { assert(rotationDegrees != null), assert(isFlipped != null), assert(sampleSize != null), - assert(regionRect != null), + assert(region != null), assert(imageSize != null), assert(scale != null); - // do not store the entry as it is, because the key should be constant - // but the entry attributes may change over time - factory RegionProviderKey.fromEntry( - ImageEntry entry, { - @required int sampleSize, - @required Rectangle rect, - }) { - return RegionProviderKey( - uri: entry.uri, - mimeType: entry.mimeType, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - page: entry.page, - sampleSize: sampleSize, - regionRect: rect, - imageSize: Size(entry.width.toDouble(), entry.height.toDouble()), - ); - } - @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.sampleSize == sampleSize && other.regionRect == regionRect && other.imageSize == imageSize && other.scale == scale; + return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale; } @override int get hashCode => hashValues( uri, mimeType, + page, rotationDegrees, isFlipped, - page, sampleSize, - regionRect, + region, imageSize, scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, sampleSize=$sampleSize, regionRect=$regionRect, imageSize=$imageSize, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}'; } diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index be69e5eca..02b4ac2f2 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -1,9 +1,8 @@ import 'dart:ui' as ui show Codec; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class ThumbnailProvider extends ImageProvider { final ThumbnailProviderKey key; @@ -35,14 +34,13 @@ class ThumbnailProvider extends ImageProvider { final page = key.page; try { final bytes = await ImageFileService.getThumbnail( - uri, - mimeType, - key.dateModifiedSecs, - key.rotationDegrees, - key.isFlipped, - key.extent, - key.extent, + uri: uri, + mimeType: mimeType, page: page, + rotationDegrees: key.rotationDegrees, + isFlipped: key.isFlipped, + dateModifiedSecs: key.dateModifiedSecs, + extent: key.extent, taskKey: key, ); if (bytes == null) { @@ -65,61 +63,49 @@ class ThumbnailProvider extends ImageProvider { } 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 dateModifiedSecs, rotationDegrees, page; + final int page, rotationDegrees; final bool isFlipped; + final int dateModifiedSecs; final double extent, scale; const ThumbnailProviderKey({ @required this.uri, @required this.mimeType, - @required this.dateModifiedSecs, + @required this.page, @required this.rotationDegrees, @required this.isFlipped, - this.page, + @required this.dateModifiedSecs, this.extent = 0, this.scale = 1, }) : assert(uri != null), assert(mimeType != null), - assert(dateModifiedSecs != null), assert(rotationDegrees != null), assert(isFlipped != null), + assert(dateModifiedSecs != null), assert(extent != null), assert(scale != null); - // do not store the entry as it is, because the key should be constant - // but the entry attributes may change over time - factory ThumbnailProviderKey.fromEntry(ImageEntry entry, {double extent = 0}) { - return ThumbnailProviderKey( - uri: entry.uri, - mimeType: entry.mimeType, - // `dateModifiedSecs` can be missing in viewer mode - dateModifiedSecs: entry.dateModifiedSecs ?? -1, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - page: entry.page, - extent: extent, - ); - } - @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is ThumbnailProviderKey && other.uri == uri && other.extent == extent && other.mimeType == mimeType && other.dateModifiedSecs == dateModifiedSecs && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale; + return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale; } @override int get hashCode => hashValues( uri, mimeType, - dateModifiedSecs, + page, rotationDegrees, isFlipped, - page, + dateModifiedSecs, extent, scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, dateModifiedSecs=$dateModifiedSecs, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, extent=$extent, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}'; } diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index 22749c5a1..4e2d3f46c 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui show Codec; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:pedantic/pedantic.dart'; class UriImage extends ImageProvider { @@ -15,7 +15,7 @@ class UriImage extends ImageProvider { const UriImage({ @required this.uri, @required this.mimeType, - this.page, + @required this.page, @required this.rotationDegrees, @required this.isFlipped, this.expectedContentLength, diff --git a/lib/image_providers/uri_picture_provider.dart b/lib/image_providers/uri_picture_provider.dart index 913c78690..f6e8dc9c1 100644 --- a/lib/image_providers/uri_picture_provider.dart +++ b/lib/image_providers/uri_picture_provider.dart @@ -1,6 +1,6 @@ import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:pedantic/pedantic.dart'; @@ -30,7 +30,7 @@ class UriPicture extends PictureProvider { Future _loadAsync(UriPicture key, {PictureErrorListener onError}) async { assert(key == this); - final data = await ImageFileService.getImage(uri, mimeType, 0, false); + final data = await ImageFileService.getSvg(uri, mimeType); if (data == null || data.isEmpty) { return null; } diff --git a/lib/main.dart b/lib/main.dart index 92cd32fbc..2445de98a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,8 +47,8 @@ class _AvesAppState extends State { // observers are not registered when using the same list object with different items // the list itself needs to be reassigned List _navigatorObservers = []; - final _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); - final _navigatorKey = GlobalKey(); + final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); + final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); static const accentColor = Colors.indigoAccent; diff --git a/lib/model/entry_cache.dart b/lib/model/entry_cache.dart index f135107e9..291cac72f 100644 --- a/lib/model/entry_cache.dart +++ b/lib/model/entry_cache.dart @@ -13,11 +13,13 @@ class EntryCache { bool oldIsFlipped, ) async { // TODO TLAD provide page parameter for multipage items, if someday image editing features are added for them + int page; // evict fullscreen image await UriImage( uri: uri, mimeType: mimeType, + page: page, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, ).evict(); @@ -26,6 +28,7 @@ class EntryCache { await ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, + page: page, dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, @@ -38,6 +41,7 @@ class EntryCache { (extent) => ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, + page: page, dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart new file mode 100644 index 000000000..74214c382 --- /dev/null +++ b/lib/model/entry_images.dart @@ -0,0 +1,63 @@ +import 'dart:math'; +import 'dart:ui'; + +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/image_entry.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraAvesEntry on ImageEntry { + ThumbnailProvider getThumbnail({double extent = 0}) => ThumbnailProvider(_getThumbnailProviderKey(extent)); + + 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(); + + return ThumbnailProviderKey( + uri: uri, + mimeType: mimeType, + page: page, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + dateModifiedSecs: dateModifiedSecs ?? -1, + extent: requestExtent, + ); + } + + RegionProvider getRegion({@required int sampleSize, Rectangle region}) => RegionProvider(getRegionProviderKey(sampleSize, region)); + + RegionProviderKey getRegionProviderKey(int sampleSize, Rectangle region) { + return RegionProviderKey( + uri: uri, + mimeType: mimeType, + page: page, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + sampleSize: sampleSize, + region: region ?? Rectangle(0, 0, width, height), + imageSize: Size(width.toDouble(), height.toDouble()), + ); + } + + UriImage get uriImage => UriImage( + uri: uri, + mimeType: mimeType, + page: page, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + expectedContentLength: sizeBytes, + ); + + bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive; + + ImageProvider getBestThumbnail(double extent) { + final sizedThumbnailKey = _getThumbnailProviderKey(extent); + if (_isReady(sizedThumbnailKey)) return ThumbnailProvider(sizedThumbnailKey); + + return getThumbnail(); + } +} diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 079c72782..121be5731 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -31,6 +31,8 @@ class ImageEntry { int sourceRotationDegrees; final int sizeBytes; String sourceTitle; + + // `dateModifiedSecs` can be missing in viewer mode int _dateModifiedSecs; final int sourceDateTakenMillis; final int durationMillis; @@ -227,7 +229,11 @@ class ImageEntry { !isAnimated && page == null; - bool get canTile => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; + bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; + + // as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved + // so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution + bool get useTiles => supportTiling && (width > 4096 || height > 4096 || is360); bool get isRaw => MimeTypes.rawImages.contains(mimeType); diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart index 53b6c8f0c..6e0bf158a 100644 --- a/lib/services/app_shortcut_service.dart +++ b/lib/services/app_shortcut_service.dart @@ -26,18 +26,18 @@ class AppShortcutService { return false; } - static Future pin(String label, ImageEntry iconEntry, Set filters) async { + static Future pin(String label, ImageEntry entry, Set filters) async { Uint8List iconBytes; - if (iconEntry != null) { - final size = iconEntry.isVideo ? 0.0 : 256.0; + if (entry != null) { + final size = entry.isVideo ? 0.0 : 256.0; iconBytes = await ImageFileService.getThumbnail( - iconEntry.uri, - iconEntry.mimeType, - iconEntry.dateModifiedSecs, - iconEntry.rotationDegrees, - iconEntry.isFlipped, - size, - size, + uri: entry.uri, + mimeType: entry.mimeType, + page: entry.page, + rotationDegrees: entry.rotationDegrees, + isFlipped: entry.isFlipped, + dateModifiedSecs: entry.dateModifiedSecs, + extent: size, ); } try { diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index d69cff77b..80cd3fc37 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -69,6 +69,21 @@ class ImageFileService { return null; } + static Future getSvg( + String uri, + String mimeType, { + int expectedContentLength, + BytesReceivedCallback onBytesReceived, + }) => + getImage( + uri, + mimeType, + 0, + false, + expectedContentLength: expectedContentLength, + onBytesReceived: onBytesReceived, + ); + static Future getImage( String uri, String mimeType, @@ -155,15 +170,14 @@ class ImageFileService { ); } - static Future getThumbnail( - String uri, - String mimeType, - int dateModifiedSecs, - int rotationDegrees, - bool isFlipped, - double width, - double height, { - int page, + static Future getThumbnail({ + @required String uri, + @required String mimeType, + @required int rotationDegrees, + @required int page, + @required bool isFlipped, + @required int dateModifiedSecs, + @required double extent, Object taskKey, int priority, }) { @@ -179,8 +193,8 @@ class ImageFileService { 'dateModifiedSecs': dateModifiedSecs, 'rotationDegrees': rotationDegrees, 'isFlipped': isFlipped, - 'widthDip': width, - 'heightDip': height, + 'widthDip': extent, + 'heightDip': extent, 'page': page, 'defaultSizeDip': thumbnailDefaultSize, }); @@ -191,7 +205,7 @@ class ImageFileService { return null; }, // debugLabel: 'getThumbnail width=$width, height=$height entry=${entry.filenameWithoutExtension}', - priority: priority ?? (width == 0 || height == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), + priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), key: taskKey, ); } diff --git a/lib/services/svg_metadata_service.dart b/lib/services/svg_metadata_service.dart index 8d09750bc..1152a74d9 100644 --- a/lib/services/svg_metadata_service.dart +++ b/lib/services/svg_metadata_service.dart @@ -17,7 +17,7 @@ class SvgMetadataService { static Future getSize(ImageEntry entry) async { try { - final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false); + final data = await ImageFileService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; @@ -59,7 +59,7 @@ class SvgMetadataService { } try { - final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false); + final data = await ImageFileService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 230e1cfea..10e922443 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -25,7 +25,7 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget { } class _FilterBarState extends State { - final GlobalKey _animatedListKey = GlobalKey(); + final GlobalKey _animatedListKey = GlobalKey(debugLabel: 'filter-bar-animated-list'); CollectionFilter _userRemovedFilter; @override diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 65c473554..8ab298158 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -1,7 +1,5 @@ -import 'dart:math'; - import 'package:aves/image_providers/thumbnail_provider.dart'; -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/error.dart'; @@ -37,11 +35,6 @@ class _RasterImageThumbnailState extends State { Object get heroTag => widget.heroTag; - // 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) - double get requestExtent => pow(2, (log(extent) / log(2)).ceil()).toDouble(); - @override void initState() { super.initState(); @@ -76,13 +69,9 @@ class _RasterImageThumbnailState extends State { void _initProvider() { if (!entry.canDecode) return; - _fastThumbnailProvider = ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry), - ); + _fastThumbnailProvider = entry.getThumbnail(); if (!entry.isVideo) { - _sizedThumbnailProvider = ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, extent: requestExtent), - ); + _sizedThumbnailProvider = entry.getThumbnail(extent: extent); } } @@ -146,22 +135,8 @@ class _RasterImageThumbnailState extends State { : Hero( tag: heroTag, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { - ImageProvider heroImageProvider = _fastThumbnailProvider; - if (!entry.isVideo) { - final imageProvider = UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: entry.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ); - if (imageCache.statusForKey(imageProvider).keepAlive) { - heroImageProvider = imageProvider; - } - } return TransitionImage( - image: heroImageProvider, + image: entry.getBestThumbnail(extent), animation: animation, ); }, diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index d9a123b67..57b086c66 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -36,7 +36,7 @@ class ThumbnailCollection extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _tileExtentNotifier = ValueNotifier(0); final ValueNotifier _isScrollingNotifier = ValueNotifier(false); - final GlobalKey _scrollableKey = GlobalKey(); + final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); static const columnCountDefault = 4; static const extentMin = 46.0; diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index 047a1a6eb..a22e2c1ba 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -15,7 +15,6 @@ class MagnifierCore extends StatefulWidget { Key key, @required this.child, @required this.onTap, - @required this.gestureDetectorBehavior, @required this.controller, @required this.scaleStateCycle, @required this.applyScale, @@ -29,7 +28,6 @@ class MagnifierCore extends StatefulWidget { final MagnifierTapCallback onTap; - final HitTestBehavior gestureDetectorBehavior; final bool applyScale; final double panInertia; diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index 22c7d83d6..61fe5a27c 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -18,106 +18,64 @@ import 'package:flutter/material.dart'; - added single & double tap position feedback - fixed focus when scaling by double-tap/pinch */ -class Magnifier extends StatefulWidget { +class Magnifier extends StatelessWidget { const Magnifier({ Key key, - @required this.child, - this.childSize, - this.controller, - this.maxScale, - this.minScale, - this.initialScale, - this.scaleStateCycle, + @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, - this.gestureDetectorBehavior, - this.applyScale, - }) : super(key: key); + @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); - final Widget child; + final MagnifierController controller; // The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. final Size childSize; - // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. - final ScaleLevel maxScale; - // Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. final ScaleLevel minScale; + // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. + final ScaleLevel maxScale; + // Defines the size the image will assume when the component is initialized, it is proportional to the original image size. final ScaleLevel initialScale; - final MagnifierController controller; final ScaleStateCycle scaleStateCycle; - final MagnifierTapCallback onTap; - final HitTestBehavior gestureDetectorBehavior; final bool applyScale; - - @override - State createState() { - return _MagnifierState(); - } -} - -class _MagnifierState extends State { - bool _controlledController; - MagnifierController _controller; - - Size get childSize => widget.childSize; - - @override - void initState() { - super.initState(); - if (widget.controller == null) { - _controlledController = true; - _controller = MagnifierController(); - } else { - _controlledController = false; - _controller = widget.controller; - } - } - - @override - void didUpdateWidget(covariant Magnifier oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.controller == null) { - if (!_controlledController) { - _controlledController = true; - _controller = MagnifierController(); - } - } else { - _controlledController = false; - _controller = widget.controller; - } - } - - @override - void dispose() { - if (_controlledController) { - _controller.dispose(); - } - super.dispose(); - } + final MagnifierTapCallback onTap; + final Widget child; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - _controller.setScaleBoundaries(ScaleBoundaries( - widget.minScale ?? 0.0, - widget.maxScale ?? ScaleLevel(factor: double.infinity), - widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained), - constraints.biggest, - widget.childSize?.isEmpty == true ? constraints.biggest : widget.childSize, + controller.setScaleBoundaries(ScaleBoundaries( + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + viewportSize: constraints.biggest, + childSize: childSize?.isEmpty == false ? childSize : constraints.biggest, )); return MagnifierCore( - child: widget.child, - controller: _controller, - scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, - onTap: widget.onTap, - gestureDetectorBehavior: widget.gestureDetectorBehavior, - applyScale: widget.applyScale ?? true, + child: child, + controller: controller, + scaleStateCycle: scaleStateCycle, + onTap: onTap, + applyScale: applyScale, ); }, ); diff --git a/lib/widgets/common/magnifier/scale/scale_boundaries.dart b/lib/widgets/common/magnifier/scale/scale_boundaries.dart index b5f565fb4..30615777a 100644 --- a/lib/widgets/common/magnifier/scale/scale_boundaries.dart +++ b/lib/widgets/common/magnifier/scale/scale_boundaries.dart @@ -7,20 +7,22 @@ import 'package:flutter/foundation.dart'; /// Internal class to wrap custom scale boundaries (min, max and initial) /// Also, stores values regarding the two sizes: the container and the child. class ScaleBoundaries { - const ScaleBoundaries( - this._minScale, - this._maxScale, - this._initialScale, - this.viewportSize, - this.childSize, - ); - final ScaleLevel _minScale; final ScaleLevel _maxScale; final ScaleLevel _initialScale; final Size viewportSize; final Size childSize; + const ScaleBoundaries({ + @required ScaleLevel minScale, + @required ScaleLevel maxScale, + @required ScaleLevel initialScale, + @required this.viewportSize, + @required this.childSize, + }) : _minScale = minScale, + _maxScale = maxScale, + _initialScale = initialScale; + double _scaleForLevel(ScaleLevel level) { final factor = level.factor; switch (level.ref) { diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 4d866db2b..fcdb2e61f 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -40,7 +40,7 @@ class FilterGridPage extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _tileExtentNotifier = ValueNotifier(0); - final GlobalKey _scrollableKey = GlobalKey(); + final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable'); static const columnCountDefault = 2; static const extentMin = 60.0; diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index 9fbe1403e..768a2883c 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -1,6 +1,6 @@ -import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/main.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/viewer/debug/db.dart'; @@ -135,18 +135,14 @@ class ViewerDebugPage extends StatelessWidget { Text('Raster (fast)'), Center( child: Image( - image: ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry), - ), + image: entry.getThumbnail(), ), ), SizedBox(height: 16), Text('Raster ($extent)'), Center( child: Image( - image: ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, extent: extent), - ), + image: entry.getThumbnail(extent: extent), ), ), ], diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index 1b95e87bd..8aea86ac8 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -158,7 +158,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { MaterialPageRoute( settings: RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( - loader: () => ImageFileService.getImage(entry.uri, entry.mimeType, 0, false).then(utf8.decode), + loader: () => ImageFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode), ), ), ); diff --git a/lib/widgets/viewer/entry_scroller.dart b/lib/widgets/viewer/entry_scroller.dart index 7f8e124b1..baa4bee88 100644 --- a/lib/widgets/viewer/entry_scroller.dart +++ b/lib/widgets/viewer/entry_scroller.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; +import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class MultiEntryScroller extends StatefulWidget { @@ -79,16 +80,22 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - EntryPageView _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page}) { - return EntryPageView( - key: Key('imageview'), - mainEntry: entry, - multiPageInfo: multiPageInfo, - page: page, - heroTag: widget.collection.heroTag(entry), - onTap: (_) => widget.onTap?.call(), - videoControllers: widget.videoControllers, - onDisposed: () => widget.onViewDisposed?.call(entry.uri), + Widget _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page}) { + return Selector( + selector: (c, mq) => mq.size, + builder: (c, mqSize, child) { + return EntryPageView( + key: Key('imageview'), + mainEntry: entry, + multiPageInfo: multiPageInfo, + page: page, + viewportSize: mqSize, + heroTag: widget.collection.heroTag(entry), + onTap: (_) => widget.onTap?.call(), + videoControllers: widget.videoControllers, + onDisposed: () => widget.onViewDisposed?.call(entry.uri), + ); + }, ); } @@ -150,13 +157,19 @@ class _SingleEntryScrollerState extends State with Automati ); } - EntryPageView _buildViewer({MultiPageInfo multiPageInfo, int page}) { - return EntryPageView( - mainEntry: entry, - multiPageInfo: multiPageInfo, - page: page, - onTap: (_) => widget.onTap?.call(), - videoControllers: widget.videoControllers, + Widget _buildViewer({MultiPageInfo multiPageInfo, int page}) { + return Selector( + selector: (c, mq) => mq.size, + builder: (c, mqSize, child) { + return EntryPageView( + mainEntry: entry, + multiPageInfo: multiPageInfo, + page: page, + viewportSize: mqSize, + onTap: (_) => widget.onTap?.call(), + videoControllers: widget.videoControllers, + ); + }, ); } diff --git a/lib/widgets/viewer/info/maps/marker.dart b/lib/widgets/viewer/info/maps/marker.dart index 9e3a42629..0cd8bc006 100644 --- a/lib/widgets/viewer/info/maps/marker.dart +++ b/lib/widgets/viewer/info/maps/marker.dart @@ -158,7 +158,7 @@ class _MarkerGeneratorWidgetState extends State { type: MaterialType.transparency, child: Stack( children: widget.markers.map((i) { - final key = GlobalKey(); + final key = GlobalKey(debugLabel: 'map-marker-$i'); _globalKeys.add(key); return RepaintBoundary( key: key, diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index eee35e659..1d3b287b5 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -28,7 +28,7 @@ class VideoControlOverlay extends StatefulWidget { } class _VideoControlOverlayState extends State with SingleTickerProviderStateMixin { - final GlobalKey _progressBarKey = GlobalKey(); + final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar'); bool _playingOnDragStart = false; AnimationController _playPauseAnimation; final List _subscriptions = []; diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index eec842376..31eeea732 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/theme/icons.dart'; @@ -72,14 +72,7 @@ class _PanoramaPageState extends State { ); }, child: Image( - image: UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: entry.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ), + image: entry.uriImage, ), ), Positioned( diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index f8f27fe6f..c43028cfd 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/metadata_service.dart'; @@ -47,37 +47,25 @@ class EntryPrinter { final multiPageInfo = await MetadataService.getMultiPageInfo(entry); if (multiPageInfo.pageCount > 1) { for (final kv in multiPageInfo.pages.entries) { - _addPdfPage(await _buildPageImage(page: kv.key)); + final pageEntry = entry.getPageEntry(multiPageInfo: multiPageInfo, page: kv.key); + _addPdfPage(await _buildPageImage(pageEntry)); } } } if (pages.isEmpty) { - _addPdfPage(await _buildPageImage()); + _addPdfPage(await _buildPageImage(entry)); } return pages; } - Future _buildPageImage({int page}) async { - final uri = entry.uri; - final mimeType = entry.mimeType; - final rotationDegrees = entry.rotationDegrees; - final isFlipped = entry.isFlipped; - + Future _buildPageImage(ImageEntry entry) async { if (entry.isSvg) { - final bytes = await ImageFileService.getImage(uri, mimeType, rotationDegrees, isFlipped); + final bytes = await ImageFileService.getSvg(entry.uri, entry.mimeType); if (bytes != null && bytes.isNotEmpty) { return pdf.SvgImage(svg: utf8.decode(bytes)); } } else { - return pdf.Image(await flutterImageProvider( - UriImage( - uri: uri, - mimeType: mimeType, - page: page, - rotationDegrees: rotationDegrees, - isFlipped: isFlipped, - ), - )); + return pdf.Image(await flutterImageProvider(entry.uriImage)); } return null; } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index bd36e6722..2e376a374 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -26,21 +26,20 @@ class EntryPageView extends StatefulWidget { final ImageEntry entry; final MultiPageInfo multiPageInfo; final int page; + final Size viewportSize; final Object heroTag; final MagnifierTapCallback onTap; final List> videoControllers; final VoidCallback onDisposed; static const decorationCheckSize = 20.0; - static const initialScale = ScaleLevel(ref: ScaleReference.contained); - static const minScale = ScaleLevel(ref: ScaleReference.contained); - static const maxScale = ScaleLevel(factor: 2.0); EntryPageView({ Key key, ImageEntry mainEntry, this.multiPageInfo, this.page, + this.viewportSize, this.heroTag, @required this.onTap, @required this.videoControllers, @@ -59,8 +58,14 @@ class _EntryPageViewState extends State { ImageEntry get entry => widget.entry; + Size get viewportSize => widget.viewportSize; + MagnifierTapCallback get onTap => widget.onTap; + static const initialScale = ScaleLevel(ref: ScaleReference.contained); + static const minScale = ScaleLevel(ref: ScaleReference.contained); + static const maxScale = ScaleLevel(factor: 2.0); + @override void initState() { super.initState(); @@ -87,13 +92,28 @@ class _EntryPageViewState extends State { } void _registerWidget() { - _viewStateNotifier.value = ViewState.zero; + // try to initialize the view state to match magnifier initial state + _viewStateNotifier.value = viewportSize != null + ? ViewState( + Offset.zero, + ScaleBoundaries( + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + viewportSize: viewportSize, + childSize: entry.displaySize, + ).initialScale, + viewportSize, + ) + : ViewState.zero; + _magnifierController = MagnifierController(); _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); } void _unregisterWidget() { + _magnifierController.dispose(); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -113,8 +133,7 @@ class _EntryPageViewState extends State { } child ??= ErrorView(onTap: () => onTap?.call(null)); - // no hero for videos, as a typical video first frame is different from its thumbnail - return widget.heroTag != null && !entry.isVideo + return widget.heroTag != null ? Hero( tag: widget.heroTag, transitionOnUserGestures: true, @@ -124,21 +143,13 @@ class _EntryPageViewState extends State { } Widget _buildRasterView() { - return Magnifier( - // key includes size and orientation to refresh when the image is rotated - key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), - child: TiledImageView( + return _buildMagnifier( + applyScale: false, + child: RasterImageView( entry: entry, viewStateNotifier: _viewStateNotifier, errorBuilder: (context, error, stackTrace) => ErrorView(onTap: () => onTap?.call(null)), ), - childSize: entry.displaySize, - controller: _magnifierController, - maxScale: EntryPageView.maxScale, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, - onTap: (c, d, s, childPosition) => onTap?.call(childPosition), - applyScale: false, ); } @@ -146,7 +157,9 @@ class _EntryPageViewState extends State { final background = settings.vectorBackground; final colorFilter = background.isColor ? ColorFilter.mode(background.color, BlendMode.dstOver) : null; - Widget child = Magnifier( + var child = _buildMagnifier( + maxScale: ScaleLevel(factor: double.infinity), + scaleStateCycle: _vectorScaleStateCycle, child: SvgPicture( UriPicture( uri: entry.uri, @@ -154,12 +167,6 @@ class _EntryPageViewState extends State { colorFilter: colorFilter, ), ), - childSize: entry.displaySize, - controller: _magnifierController, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, - scaleStateCycle: _vectorScaleStateCycle, - onTap: (c, d, s, childPosition) => onTap?.call(childPosition), ); if (background == EntryBackground.checkered) { @@ -174,19 +181,33 @@ class _EntryPageViewState extends State { Widget _buildVideoView() { final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; + if (videoController == null) return SizedBox(); + return _buildMagnifier( + child: VideoView( + entry: entry, + controller: videoController, + ), + ); + } + + Widget _buildMagnifier({ + ScaleLevel maxScale = maxScale, + ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, + bool applyScale = true, + @required Widget child, + }) { return Magnifier( - child: videoController != null - ? AvesVideo( - entry: entry, - controller: videoController, - ) - : SizedBox(), - childSize: entry.displaySize, + // key includes size and orientation to refresh when the image is rotated + key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), controller: _magnifierController, - maxScale: EntryPageView.maxScale, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, + childSize: entry.displaySize, + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + scaleStateCycle: scaleStateCycle, + applyScale: applyScale, onTap: (c, d, s, childPosition) => onTap?.call(childPosition), + child: child, ); } diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 9b0262e23..36124bcef 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -1,12 +1,12 @@ import 'dart:math'; 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_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/entry_background.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'; @@ -14,22 +14,22 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -class TiledImageView extends StatefulWidget { +class RasterImageView extends StatefulWidget { final ImageEntry entry; final ValueNotifier viewStateNotifier; final ImageErrorWidgetBuilder errorBuilder; - const TiledImageView({ + const RasterImageView({ @required this.entry, @required this.viewStateNotifier, @required this.errorBuilder, }); @override - _TiledImageViewState createState() => _TiledImageViewState(); + _RasterImageViewState createState() => _RasterImageViewState(); } -class _TiledImageViewState extends State { +class _RasterImageViewState extends State { Size _displaySize; bool _isTilingInitialized = false; int _maxSampleSize; @@ -45,42 +45,16 @@ class _TiledImageViewState extends State { bool get useBackground => entry.canHaveAlpha && settings.rasterBackground != EntryBackground.transparent; - // as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved - // so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution - bool get useTiles => entry.canTile && (entry.displaySize.longestSide > 4096 || entry.is360); + ViewState get viewState => viewStateNotifier.value; - ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry)); + ImageProvider get thumbnailProvider => entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)); ImageProvider get fullImageProvider { - if (useTiles) { + if (entry.useTiles) { assert(_isTilingInitialized); - final displayWidth = _displaySize.width.round(); - final displayHeight = _displaySize.height.round(); - final viewState = viewStateNotifier.value; - final regionRect = _getTileRects( - x: 0, - y: 0, - layerRegionWidth: displayWidth, - layerRegionHeight: displayHeight, - displayWidth: displayWidth, - displayHeight: displayHeight, - scale: viewState.scale, - viewRect: _getViewRect(viewState, displayWidth, displayHeight), - )?.item2; - return RegionProvider(RegionProviderKey.fromEntry( - entry, - sampleSize: _maxSampleSize, - rect: regionRect, - )); + return entry.getRegion(sampleSize: _maxSampleSize); } else { - return UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: entry.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ); + return entry.uriImage; } } @@ -92,11 +66,11 @@ class _TiledImageViewState extends State { super.initState(); _displaySize = entry.displaySize; _fullImageListener = ImageStreamListener(_onFullImageCompleted); - if (!useTiles) _registerFullImage(); + if (!entry.useTiles) _registerFullImage(); } @override - void didUpdateWidget(covariant TiledImageView oldWidget) { + void didUpdateWidget(covariant RasterImageView oldWidget) { super.didUpdateWidget(oldWidget); final oldViewState = oldWidget.viewStateNotifier.value; @@ -133,11 +107,12 @@ class _TiledImageViewState extends State { 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 != null; + final viewportSized = viewportSize?.isEmpty == false; if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize); return SizedBox.fromSize( @@ -145,9 +120,9 @@ class _TiledImageViewState extends State { child: Stack( alignment: Alignment.center, children: [ - if (useBackground && viewportSized) _buildBackground(viewState), - _buildLoading(viewState), - if (useTiles) ..._getTiles(viewState), + if (useBackground && viewportSized) _buildBackground(), + _buildLoading(), + if (useTiles) ..._getTiles(), if (!useTiles) Image( image: fullImageProvider, @@ -184,7 +159,7 @@ class _TiledImageViewState extends State { _registerFullImage(); } - Widget _buildLoading(ViewState viewState) { + Widget _buildLoading() { return ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { @@ -204,7 +179,7 @@ class _TiledImageViewState extends State { ); } - Widget _buildBackground(ViewState viewState) { + Widget _buildBackground() { final viewportSize = viewState.viewportSize; assert(viewportSize != null); @@ -212,19 +187,30 @@ class _TiledImageViewState extends State { final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position; final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source; - Decoration decoration; + Widget child; final background = settings.rasterBackground; if (background == EntryBackground.checkered) { final side = viewportSize.shortestSide; final checkSize = side / ((side / EntryPageView.decorationCheckSize).round()); final offset = ((decorationSize - viewportSize) as Offset) / 2; - decoration = CheckeredDecoration( - checkSize: checkSize, - offset: offset, + child = ValueListenableBuilder( + valueListenable: _fullImageLoaded, + builder: (context, fullImageLoaded, child) { + if (!fullImageLoaded) return SizedBox.shrink(); + + return DecoratedBox( + decoration: CheckeredDecoration( + checkSize: checkSize, + offset: offset, + ), + ); + }, ); } else { - decoration = BoxDecoration( - color: background.color, + child = DecoratedBox( + decoration: BoxDecoration( + color: background.color, + ), ); } return Positioned( @@ -232,36 +218,37 @@ class _TiledImageViewState extends State { top: decorationOffset.dy >= 0 ? decorationOffset.dy : null, width: decorationSize.width, height: decorationSize.height, - child: DecoratedBox( - decoration: decoration, - ), + child: child, ); } - List _getTiles(ViewState viewState) { + List _getTiles() { if (!_isTilingInitialized) return []; final displayWidth = _displaySize.width.round(); final displayHeight = _displaySize.height.round(); - final viewRect = _getViewRect(viewState, displayWidth, displayHeight); + final viewRect = _getViewRect(displayWidth, displayHeight); final scale = viewState.scale; - final tiles = []; + // for the largest sample size (matching the initial scale), the whole image is in view + // so we subsample the whole image without tiling + final fullImageRegionTile = RegionTile( + entry: entry, + tileRect: Rect.fromLTWH(0, 0, displayWidth * scale, displayHeight * scale), + sampleSize: _maxSampleSize, + ); + final tiles = [fullImageRegionTile]; + var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize); - for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) { - // for the largest sample size (matching the initial scale), the whole image is in view - // so we subsample the whole image without tiling - final fullImageRegion = sampleSize == _maxSampleSize; + int nextSampleSize(int sampleSize) => (sampleSize / 2).floor(); + for (var sampleSize = nextSampleSize(_maxSampleSize); sampleSize >= minSampleSize; sampleSize = nextSampleSize(sampleSize)) { final regionSide = (_tileSide * sampleSize).round(); - final layerRegionWidth = fullImageRegion ? displayWidth : regionSide; - final layerRegionHeight = fullImageRegion ? displayHeight : regionSide; - for (var x = 0; x < displayWidth; x += layerRegionWidth) { - for (var y = 0; y < displayHeight; y += layerRegionHeight) { + for (var x = 0; x < displayWidth; x += regionSide) { + for (var y = 0; y < displayHeight; y += regionSide) { final rects = _getTileRects( x: x, y: y, - layerRegionWidth: layerRegionWidth, - layerRegionHeight: layerRegionHeight, + regionSide: regionSide, displayWidth: displayWidth, displayHeight: displayHeight, scale: scale, @@ -281,7 +268,7 @@ class _TiledImageViewState extends State { return tiles; } - Rect _getViewRect(ViewState viewState, int displayWidth, int displayHeight) { + Rect _getViewRect(int displayWidth, int displayHeight) { final scale = viewState.scale; final centerOffset = viewState.position; final viewportSize = viewState.viewportSize; @@ -295,17 +282,16 @@ class _TiledImageViewState extends State { Tuple2> _getTileRects({ @required int x, @required int y, - @required int layerRegionWidth, - @required int layerRegionHeight, + @required int regionSide, @required int displayWidth, @required int displayHeight, @required double scale, @required Rect viewRect, }) { - final nextX = x + layerRegionWidth; - final nextY = y + layerRegionHeight; - final thisRegionWidth = layerRegionWidth - (nextX >= displayWidth ? nextX - displayWidth : 0); - final thisRegionHeight = layerRegionHeight - (nextY >= displayHeight ? nextY - displayHeight : 0); + final nextX = x + regionSide; + final nextY = y + regionSide; + final thisRegionWidth = regionSide - (nextX >= displayWidth ? nextX - displayWidth : 0); + final thisRegionHeight = regionSide - (nextY >= displayHeight ? nextY - displayHeight : 0); final tileRect = Rect.fromLTWH(x * scale, y * scale, thisRegionWidth * scale, thisRegionHeight * scale); // only build visible tiles @@ -348,12 +334,21 @@ class RegionTile extends StatefulWidget { const RegionTile({ @required this.entry, @required this.tileRect, - @required this.regionRect, + this.regionRect, @required this.sampleSize, }); @override _RegionTileState createState() => _RegionTileState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('contentId', entry.contentId)); + properties.add(DiagnosticsProperty('tileRect', tileRect)); + properties.add(DiagnosticsProperty>('regionRect', regionRect)); + properties.add(IntProperty('sampleSize', sampleSize)); + } } class _RegionTileState extends State { @@ -393,11 +388,10 @@ class _RegionTileState extends State { void _initProvider() { if (!entry.canDecode) return; - _provider = RegionProvider(RegionProviderKey.fromEntry( - entry, + _provider = entry.getRegion( sampleSize: widget.sampleSize, - rect: widget.regionRect, - )); + region: widget.regionRect, + ); } void _pauseProvider() => _provider?.pause(); @@ -440,12 +434,4 @@ class _RegionTileState extends State { child: child, ); } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(IntProperty('contentId', widget.entry.contentId)); - properties.add(IntProperty('sampleSize', widget.sampleSize)); - properties.add(DiagnosticsProperty>('regionRect', widget.regionRect)); - } } diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 66b67b1e2..28e5937e4 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -1,26 +1,26 @@ import 'dart:async'; import 'dart:ui'; -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; -class AvesVideo extends StatefulWidget { +class VideoView extends StatefulWidget { final ImageEntry entry; final IjkMediaController controller; - const AvesVideo({ + const VideoView({ Key key, @required this.entry, @required this.controller, }) : super(key: key); @override - State createState() => _AvesVideoState(); + State createState() => _VideoViewState(); } -class _AvesVideoState extends State { +class _VideoViewState extends State { final List _subscriptions = []; ImageEntry get entry => widget.entry; @@ -34,7 +34,7 @@ class _AvesVideoState extends State { } @override - void didUpdateWidget(covariant AvesVideo oldWidget) { + void didUpdateWidget(covariant VideoView oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -46,11 +46,11 @@ class _AvesVideoState extends State { super.dispose(); } - void _registerWidget(AvesVideo widget) { + void _registerWidget(VideoView widget) { _subscriptions.add(widget.controller.playFinishStream.listen(_onPlayFinish)); } - void _unregisterWidget(AvesVideo widget) { + void _unregisterWidget(VideoView widget) { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -98,16 +98,8 @@ class _AvesVideoState extends State { backgroundColor: Colors.transparent, ) : Image( - image: UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: entry.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ), - width: entry.width.toDouble(), - height: entry.height.toDouble(), + image: entry.getBestThumbnail(entry.displaySize.longestSide), + fit: BoxFit.contain, ); }); } From c252ce78286a9d79aa070c538f62afa43fff75cf Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 21 Jan 2021 12:20:31 +0900 Subject: [PATCH 14/44] renaming --- .../aves/channel/calls/ImageFileHandler.kt | 10 ++--- .../channel/streams/ImageOpStreamHandler.kt | 4 +- .../model/{AvesImageEntry.kt => AvesEntry.kt} | 2 +- .../{SourceImageEntry.kt => SourceEntry.kt} | 4 +- .../model/provider/ContentImageProvider.kt | 4 +- .../aves/model/provider/FileImageProvider.kt | 4 +- .../aves/model/provider/ImageProvider.kt | 4 +- .../model/provider/MediaStoreImageProvider.kt | 8 ++-- lib/model/{image_entry.dart => entry.dart} | 22 +++++----- lib/model/entry_images.dart | 4 +- lib/model/favourite_repo.dart | 14 +++---- lib/model/filters/album.dart | 4 +- lib/model/filters/favourite.dart | 4 +- lib/model/filters/filters.dart | 6 +-- lib/model/filters/location.dart | 4 +- lib/model/filters/mime.dart | 6 +-- lib/model/filters/query.dart | 6 +-- lib/model/filters/tag.dart | 4 +- .../{image_metadata.dart => metadata.dart} | 0 lib/model/metadata_db.dart | 14 +++---- lib/model/multipage.dart | 4 +- lib/model/source/album.dart | 4 +- lib/model/source/collection_lens.dart | 42 +++++++++---------- lib/model/source/collection_source.dart | 30 ++++++------- lib/model/source/location.dart | 10 ++--- lib/model/source/media_store_source.dart | 8 ++-- lib/model/source/tag.dart | 6 +-- lib/services/android_app_service.dart | 6 +-- lib/services/android_debug_service.dart | 14 +++---- lib/services/app_shortcut_service.dart | 4 +- lib/services/image_file_service.dart | 30 ++++++------- lib/services/metadata_service.dart | 18 ++++---- lib/services/svg_metadata_service.dart | 6 +-- .../collection/entry_set_action_delegate.dart | 4 +- .../collection/grid/section_layout.dart | 8 ++-- lib/widgets/collection/grid/selector.dart | 8 ++-- lib/widgets/collection/grid/thumbnail.dart | 4 +- .../collection/thumbnail/decorated.dart | 4 +- lib/widgets/collection/thumbnail/error.dart | 4 +- lib/widgets/collection/thumbnail/overlay.dart | 10 ++--- lib/widgets/collection/thumbnail/raster.dart | 6 +-- lib/widgets/collection/thumbnail/vector.dart | 4 +- .../collection/thumbnail_collection.dart | 8 ++-- .../common/action_mixins/feedback.dart | 4 +- .../action_mixins/permission_aware.dart | 4 +- .../common/action_mixins/size_aware.dart | 6 +-- lib/widgets/common/identity/aves_icons.dart | 4 +- lib/widgets/debug/app_debug_page.dart | 4 +- lib/widgets/debug/database.dart | 6 +-- lib/widgets/dialogs/rename_entry_dialog.dart | 6 +-- .../common/decorated_filter_chip.dart | 4 +- lib/widgets/home_page.dart | 8 ++-- lib/widgets/stats/stats.dart | 6 +-- lib/widgets/viewer/debug/db.dart | 12 +++--- lib/widgets/viewer/debug/metadata.dart | 6 +-- lib/widgets/viewer/debug_page.dart | 4 +- lib/widgets/viewer/entry_action_delegate.dart | 16 +++---- lib/widgets/viewer/entry_scroller.dart | 14 +++---- lib/widgets/viewer/entry_viewer_page.dart | 6 +-- lib/widgets/viewer/entry_viewer_stack.dart | 20 ++++----- lib/widgets/viewer/info/basic_section.dart | 4 +- lib/widgets/viewer/info/info_app_bar.dart | 4 +- lib/widgets/viewer/info/info_page.dart | 12 +++--- lib/widgets/viewer/info/info_search.dart | 4 +- lib/widgets/viewer/info/location_section.dart | 10 ++--- lib/widgets/viewer/info/maps/marker.dart | 4 +- .../info/metadata/metadata_dir_tile.dart | 4 +- .../info/metadata/metadata_section.dart | 6 +-- .../info/metadata/metadata_thumbnail.dart | 6 +-- .../viewer/info/metadata/xmp_tile.dart | 8 ++-- lib/widgets/viewer/multipage.dart | 4 +- lib/widgets/viewer/overlay/bottom.dart | 18 ++++---- lib/widgets/viewer/overlay/minimap.dart | 4 +- lib/widgets/viewer/overlay/multipage.dart | 8 ++-- lib/widgets/viewer/overlay/panorama.dart | 4 +- lib/widgets/viewer/overlay/top.dart | 8 ++-- lib/widgets/viewer/overlay/video.dart | 6 +-- lib/widgets/viewer/panorama_page.dart | 6 +-- lib/widgets/viewer/printer.dart | 6 +-- .../viewer/visual/entry_page_view.dart | 8 ++-- lib/widgets/viewer/visual/raster.dart | 10 ++--- lib/widgets/viewer/visual/video.dart | 6 +-- 82 files changed, 325 insertions(+), 325 deletions(-) rename android/app/src/main/kotlin/deckers/thibault/aves/model/{AvesImageEntry.kt => AvesEntry.kt} (91%) rename android/app/src/main/kotlin/deckers/thibault/aves/model/{SourceImageEntry.kt => SourceEntry.kt} (99%) rename lib/model/{image_entry.dart => entry.dart} (98%) rename lib/model/{image_metadata.dart => metadata.dart} (100%) 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 de0ee5245..82edbd66c 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 @@ -27,7 +27,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "getObsoleteEntries" -> GlobalScope.launch(Dispatchers.IO) { getObsoleteEntries(call, Coresult(result)) } - "getImageEntry" -> GlobalScope.launch(Dispatchers.IO) { getImageEntry(call, Coresult(result)) } + "getEntry" -> GlobalScope.launch(Dispatchers.IO) { getEntry(call, Coresult(result)) } "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { getThumbnail(call, Coresult(result)) } "getRegion" -> GlobalScope.launch(Dispatchers.IO) { getRegion(call, Coresult(result)) } "clearSizedThumbnailDiskCache" -> { @@ -119,23 +119,23 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { } } - private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) { + private suspend 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) { - result.error("getImageEntry-args", "failed because of missing arguments", null) + result.error("getEntry-args", "failed because of missing arguments", null) return } val provider = getProvider(uri) if (provider == null) { - result.error("getImageEntry-provider", "failed to find provider for uri=$uri", null) + result.error("getEntry-provider", "failed to find provider for uri=$uri", null) return } provider.fetchSingle(activity, uri, mimeType, object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = result.success(fields) - override fun onFailure(throwable: Throwable) = result.error("getImageEntry-failure", "failed to get entry for uri=$uri", throwable.message) + override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message) }) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index 503fad11e..de45c8817 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -5,7 +5,7 @@ import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log -import deckers.thibault.aves.model.AvesImageEntry +import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.provider.FieldMap import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider @@ -102,7 +102,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments: } destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) - val entries = entryMapList.map(::AvesImageEntry) + val entries = entryMapList.map(::AvesEntry) provider.moveMultiple(context, copy, destinationDir, entries, object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = success(fields) override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt similarity index 91% rename from android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt index 06adb5d03..e942c2722 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesImageEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt @@ -3,7 +3,7 @@ package deckers.thibault.aves.model import android.net.Uri import deckers.thibault.aves.model.provider.FieldMap -class AvesImageEntry(map: FieldMap) { +class AvesEntry(map: FieldMap) { val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI val path = map["path"] as String? // best effort to get local path val mimeType = map["mimeType"] as String diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt similarity index 99% rename from android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt index 2e963ae6a..a482087ac 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt @@ -31,7 +31,7 @@ import deckers.thibault.aves.utils.StorageUtils import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.IOException -class SourceImageEntry { +class SourceEntry { val uri: Uri // content or file URI var path: String? = null // best effort to get local path private val sourceMimeType: String @@ -119,7 +119,7 @@ class SourceImageEntry { // metadata retrieval // expects entry with: uri, mimeType // finds: width, height, orientation/rotation, date, title, duration - fun fillPreCatalogMetadata(context: Context): SourceImageEntry { + fun fillPreCatalogMetadata(context: Context): SourceEntry { if (isSvg) return this if (isVideo) { fillVideoByMediaMetadataRetriever(context) 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 006e61e2f..96b49e980 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 @@ -3,7 +3,7 @@ package deckers.thibault.aves.model.provider import android.content.Context import android.net.Uri import android.provider.MediaStore -import deckers.thibault.aves.model.SourceImageEntry +import deckers.thibault.aves.model.SourceEntry internal class ContentImageProvider : ImageProvider() { override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { @@ -28,7 +28,7 @@ internal class ContentImageProvider : ImageProvider() { return } - val entry = SourceImageEntry(map).fillPreCatalogMetadata(context) + val entry = SourceEntry(map).fillPreCatalogMetadata(context) if (entry.isSized || entry.isSvg) { callback.onSuccess(entry.toMap()) } else { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt index f47adb072..7a08724bf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt @@ -2,7 +2,7 @@ package deckers.thibault.aves.model.provider import android.content.Context import android.net.Uri -import deckers.thibault.aves.model.SourceImageEntry +import deckers.thibault.aves.model.SourceEntry import java.io.File internal class FileImageProvider : ImageProvider() { @@ -12,7 +12,7 @@ internal class FileImageProvider : ImageProvider() { return } - val entry = SourceImageEntry(uri, mimeType) + val entry = SourceEntry(uri, mimeType) val path = uri.path if (path != null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index 8814ee5c7..fd8421607 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -8,7 +8,7 @@ import android.provider.MediaStore import android.util.Log import androidx.exifinterface.media.ExifInterface import com.commonsware.cwac.document.DocumentFileCompat -import deckers.thibault.aves.model.AvesImageEntry +import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes.isImage @@ -32,7 +32,7 @@ abstract class ImageProvider { throw UnsupportedOperationException() } - open suspend fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List, callback: ImageOpCallback) { + open suspend fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List, callback: ImageOpCallback) { callback.onFailure(UnsupportedOperationException()) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 7329453a0..f06b5ea04 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -8,8 +8,8 @@ import android.os.Build import android.provider.MediaStore import android.util.Log import com.commonsware.cwac.document.DocumentFileCompat -import deckers.thibault.aves.model.AvesImageEntry -import deckers.thibault.aves.model.SourceImageEntry +import deckers.thibault.aves.model.AvesEntry +import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isImage @@ -158,7 +158,7 @@ class MediaStoreImageProvider : ImageProvider() { // missing some attributes such as width, height, orientation. // Also, the reported size of raw images is inconsistent across devices // and Android versions (sometimes the raw size, sometimes the decoded size). - val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context) + val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context) entryMap = entry.toMap() } @@ -203,7 +203,7 @@ class MediaStoreImageProvider : ImageProvider() { context: Context, copy: Boolean, destinationDir: String, - entries: List, + entries: List, callback: ImageOpCallback, ) { val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir) diff --git a/lib/model/image_entry.dart b/lib/model/entry.dart similarity index 98% rename from lib/model/image_entry.dart rename to lib/model/entry.dart index 121be5731..6d3588236 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/entry.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:aves/model/entry_cache.dart'; import 'package:aves/model/favourite_repo.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/services/image_file_service.dart'; @@ -21,7 +21,7 @@ import 'package:path/path.dart' as ppath; import '../ref/mime_types.dart'; -class ImageEntry { +class AvesEntry { String uri; String _path, _directory, _filename, _extension; int page, contentId; @@ -45,7 +45,7 @@ class ImageEntry { // TODO TLAD make it dynamic if it depends on OS/lib versions static const List undecodable = [MimeTypes.crw, MimeTypes.psd]; - ImageEntry({ + AvesEntry({ this.uri, String path, this.contentId, @@ -69,14 +69,14 @@ class ImageEntry { bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType); - ImageEntry copyWith({ + AvesEntry copyWith({ @required String uri, @required String path, @required int contentId, @required int dateModifiedSecs, }) { final copyContentId = contentId ?? this.contentId; - final copied = ImageEntry( + final copied = AvesEntry( uri: uri ?? uri, path: path ?? this.path, contentId: copyContentId, @@ -96,7 +96,7 @@ class ImageEntry { return copied; } - ImageEntry getPageEntry({ + AvesEntry getPageEntry({ @required MultiPageInfo multiPageInfo, @required int page, }) { @@ -126,8 +126,8 @@ class ImageEntry { } // from DB or platform source entry - factory ImageEntry.fromMap(Map map) { - return ImageEntry( + factory AvesEntry.fromMap(Map map) { + return AvesEntry( uri: map['uri'] as String, path: map['path'] as String, contentId: map['contentId'] as int, @@ -619,7 +619,7 @@ class ImageEntry { // compare by: // 1) title ascending // 2) extension ascending - static int compareByName(ImageEntry a, ImageEntry b) { + static int compareByName(AvesEntry a, AvesEntry b) { final c = compareAsciiUpperCase(a.bestTitle, b.bestTitle); return c != 0 ? c : compareAsciiUpperCase(a.extension, b.extension); } @@ -627,7 +627,7 @@ class ImageEntry { // compare by: // 1) size descending // 2) name ascending - static int compareBySize(ImageEntry a, ImageEntry b) { + static int compareBySize(AvesEntry a, AvesEntry b) { final c = b.sizeBytes.compareTo(a.sizeBytes); return c != 0 ? c : compareByName(a, b); } @@ -637,7 +637,7 @@ class ImageEntry { // compare by: // 1) date descending // 2) name ascending - static int compareByDate(ImageEntry a, ImageEntry b) { + static int compareByDate(AvesEntry a, AvesEntry b) { final c = (b.bestDate ?? _epoch).compareTo(a.bestDate ?? _epoch); return c != 0 ? c : compareByName(a, b); } diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart index 74214c382..85bd93828 100644 --- a/lib/model/entry_images.dart +++ b/lib/model/entry_images.dart @@ -4,11 +4,11 @@ import 'dart:ui'; 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/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -extension ExtraAvesEntry on ImageEntry { +extension ExtraAvesEntry on AvesEntry { ThumbnailProvider getThumbnail({double extent = 0}) => ThumbnailProvider(_getThumbnailProviderKey(extent)); ThumbnailProviderKey _getThumbnailProviderKey(double extent) { diff --git a/lib/model/favourite_repo.dart b/lib/model/favourite_repo.dart index d138de273..78daf8883 100644 --- a/lib/model/favourite_repo.dart +++ b/lib/model/favourite_repo.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/utils/change_notifier.dart'; @@ -18,25 +18,25 @@ class FavouriteRepo { int get count => _rows.length; - bool isFavourite(ImageEntry entry) => _rows.any((row) => row.contentId == entry.contentId); + bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId); - FavouriteRow _entryToRow(ImageEntry entry) => FavouriteRow(contentId: entry.contentId, path: entry.path); + FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId, path: entry.path); - Future add(Iterable entries) async { + Future add(Iterable entries) async { final newRows = entries.map(_entryToRow); await metadataDb.addFavourites(newRows); _rows.addAll(newRows); changeNotifier.notifyListeners(); } - Future remove(Iterable entries) async { + Future remove(Iterable entries) async { final removedRows = entries.map(_entryToRow); await metadataDb.removeFavourites(removedRows); removedRows.forEach(_rows.remove); changeNotifier.notifyListeners(); } - Future move(int oldContentId, ImageEntry entry) async { + Future move(int oldContentId, AvesEntry entry) async { final oldRow = _rows.firstWhere((row) => row.contentId == oldContentId, orElse: () => null); if (oldRow != null) { _rows.remove(oldRow); diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 82f3bcc56..1ab6ce062 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -1,6 +1,6 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; @@ -33,7 +33,7 @@ class AlbumFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => entry.directory == album; + bool filter(AvesEntry entry) => entry.directory == album; @override String get label => uniqueName ?? album.split(separator).last; diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index d4e9716c1..c64326959 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -13,7 +13,7 @@ class FavouriteFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => entry.isFavourite; + bool filter(AvesEntry entry) => entry.isFavourite; @override String get label => 'Favourite'; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index e70f39dd8..a3e985302 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -1,12 +1,12 @@ import 'dart:convert'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -49,7 +49,7 @@ abstract class CollectionFilter implements Comparable { String toJson() => jsonEncode(toMap()); - bool filter(ImageEntry entry); + bool filter(AvesEntry entry); bool get isUnique => true; @@ -78,7 +78,7 @@ abstract class CollectionFilter implements Comparable { // TODO TLAD replace this by adding getters to CollectionFilter, with cached entry/count coming from Source class FilterGridItem { final T filter; - final ImageEntry 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 cf46da1b2..c702cf2c4 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -35,7 +35,7 @@ class LocationFilter extends CollectionFilter { String get countryNameAndCode => '$_location$locationSeparator$_countryCode'; @override - bool filter(ImageEntry entry) => _location.isEmpty ? !entry.isLocated : entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryCode == _countryCode) || (level == LocationLevel.place && entry.addressDetails.place == _location)); + bool filter(AvesEntry entry) => _location.isEmpty ? !entry.isLocated : entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryCode == _countryCode) || (level == LocationLevel.place && entry.addressDetails.place == _location)); @override String get label => _location.isEmpty ? emptyLabel : _location; diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 2b3342140..5944c27df 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -1,5 +1,5 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:flutter/foundation.dart'; @@ -15,7 +15,7 @@ class MimeFilter extends CollectionFilter { static const geotiff = 'aves/geotiff'; // subset of `image/tiff` final String mime; - bool Function(ImageEntry) _filter; + bool Function(AvesEntry) _filter; String _label; IconData _icon; @@ -67,7 +67,7 @@ class MimeFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => _filter(entry); + bool filter(AvesEntry entry) => _filter(entry); @override String get label => _label; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index bb880cb2f..33fe221e4 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -12,7 +12,7 @@ class QueryFilter extends CollectionFilter { final String query; final bool colorful; - bool Function(ImageEntry) _filter; + bool Function(AvesEntry) _filter; QueryFilter(this.query, {this.colorful = true}) { var upQuery = query.toUpperCase(); @@ -44,7 +44,7 @@ class QueryFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => _filter(entry); + bool filter(AvesEntry entry) => _filter(entry); @override bool get isUnique => false; diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index ff9e94611..5d21f0b7f 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -24,7 +24,7 @@ class TagFilter extends CollectionFilter { }; @override - bool filter(ImageEntry entry) => tag.isEmpty ? entry.xmpSubjects.isEmpty : entry.xmpSubjects.contains(tag); + bool filter(AvesEntry entry) => tag.isEmpty ? entry.xmpSubjects.isEmpty : entry.xmpSubjects.contains(tag); @override bool get isUnique => false; diff --git a/lib/model/image_metadata.dart b/lib/model/metadata.dart similarity index 100% rename from lib/model/image_metadata.dart rename to lib/model/metadata.dart diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index 48a8b5a3b..3630f5a27 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db_upgrade.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; @@ -116,16 +116,16 @@ class MetadataDb { debugPrint('$runtimeType clearEntries deleted $count entries'); } - Future> loadEntries() async { + Future> loadEntries() async { final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(entryTable); - final entries = maps.map((map) => ImageEntry.fromMap(map)).toList(); + final entries = maps.map((map) => AvesEntry.fromMap(map)).toList(); debugPrint('$runtimeType loadEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries'); return entries; } - Future saveEntries(Iterable entries) async { + Future saveEntries(Iterable entries) async { if (entries == null || entries.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; @@ -135,7 +135,7 @@ class MetadataDb { debugPrint('$runtimeType saveEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries'); } - Future updateEntryId(int oldId, ImageEntry entry) async { + Future updateEntryId(int oldId, AvesEntry entry) async { final db = await _database; final batch = db.batch(); batch.delete(entryTable, where: 'contentId = ?', whereArgs: [oldId]); @@ -143,7 +143,7 @@ class MetadataDb { await batch.commit(noResult: true); } - void _batchInsertEntry(Batch batch, ImageEntry entry) { + void _batchInsertEntry(Batch batch, AvesEntry entry) { if (entry == null) return; batch.insert( entryTable, diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index 45af27276..e61bc0f8a 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:flutter/foundation.dart'; class MultiPageInfo { @@ -50,7 +50,7 @@ class SinglePageInfo { String toString() => '$runtimeType#${shortHash(this)}{mimeType=$mimeType, width=$width, height=$height, trackId=$trackId, durationMillis=$durationMillis}'; } -class AvesPageEntry extends ImageEntry { +class AvesPageEntry extends AvesEntry { final SinglePageInfo pageInfo; AvesPageEntry({ diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 0dce9f30f..b50aa3777 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -1,5 +1,5 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/android_file_utils.dart'; @@ -50,7 +50,7 @@ mixin AlbumMixin on SourceBase { } } - Map getAlbumEntries() { + Map getAlbumEntries() { final entries = sortedEntriesForFilterList; final regularAlbums = [], appAlbums = [], specialAlbums = []; for (var album in sortedAlbums) { diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index f56893da9..de6e12bf7 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:collection'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/tag.dart'; @@ -20,10 +20,10 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel EntrySortFactor sortFactor; final AChangeNotifier filterChangeNotifier = AChangeNotifier(); - List _filteredEntries; + List _filteredEntries; List _subscriptions = []; - Map> sections = Map.unmodifiable({}); + Map> sections = Map.unmodifiable({}); CollectionLens({ @required this.source, @@ -63,9 +63,9 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel int get entryCount => _filteredEntries.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 { + List get sortedEntries { _sortedEntries ??= List.of(sections.entries.expand((e) => e.value)); return _sortedEntries; } @@ -82,7 +82,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel return true; } - Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}'; + Object heroTag(AvesEntry entry) => '$hashCode${entry.uri}'; void addFilter(CollectionFilter filter) { if (filter == null || filters.contains(filter)) return; @@ -123,13 +123,13 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel void _applySort() { switch (sortFactor) { case EntrySortFactor.date: - _filteredEntries.sort(ImageEntry.compareByDate); + _filteredEntries.sort(AvesEntry.compareByDate); break; case EntrySortFactor.size: - _filteredEntries.sort(ImageEntry.compareBySize); + _filteredEntries.sort(AvesEntry.compareBySize); break; case EntrySortFactor.name: - _filteredEntries.sort(ImageEntry.compareByName); + _filteredEntries.sort(AvesEntry.compareByName); break; } } @@ -139,13 +139,13 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel case EntrySortFactor.date: switch (groupFactor) { case EntryGroupFactor.album: - sections = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); + sections = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); break; case EntryGroupFactor.month: - sections = groupBy(_filteredEntries, (entry) => EntryDateSectionKey(entry.monthTaken)); + sections = groupBy(_filteredEntries, (entry) => EntryDateSectionKey(entry.monthTaken)); break; case EntryGroupFactor.day: - sections = groupBy(_filteredEntries, (entry) => EntryDateSectionKey(entry.dayTaken)); + sections = groupBy(_filteredEntries, (entry) => EntryDateSectionKey(entry.dayTaken)); break; case EntryGroupFactor.none: sections = Map.fromEntries([ @@ -160,8 +160,8 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel ]); break; case EntrySortFactor.name: - final byAlbum = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); - sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.folderPath, b.folderPath)); + final byAlbum = groupBy(_filteredEntries, (entry) => EntryAlbumSectionKey(entry.directory)); + sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.folderPath, b.folderPath)); break; } sections = Map.unmodifiable(sections); @@ -177,7 +177,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel _applyGroup(); } - void onEntryRemoved(Iterable entries) { + void onEntryRemoved(Iterable entries) { // we should remove obsolete entries and sections // but do not apply sort/group // as section order change would surprise the user while browsing @@ -207,18 +207,18 @@ mixin CollectionActivityMixin { mixin CollectionSelectionMixin on CollectionActivityMixin { final AChangeNotifier selectionChangeNotifier = AChangeNotifier(); - final Set _selection = {}; + final Set _selection = {}; - Set get selection => _selection; + Set get selection => _selection; - bool isSelected(Iterable entries) => entries.every(selection.contains); + bool isSelected(Iterable entries) => entries.every(selection.contains); - void addToSelection(Iterable entries) { + void addToSelection(Iterable entries) { _selection.addAll(entries); selectionChangeNotifier.notifyListeners(); } - void removeFromSelection(Iterable entries) { + void removeFromSelection(Iterable entries) { _selection.removeAll(entries); selectionChangeNotifier.notifyListeners(); } @@ -228,7 +228,7 @@ mixin CollectionSelectionMixin on CollectionActivityMixin { selectionChangeNotifier.notifyListeners(); } - void toggleSelection(ImageEntry entry) { + void toggleSelection(AvesEntry entry) { if (_selection.isEmpty) select(); if (!_selection.remove(entry)) _selection.add(entry); selectionChangeNotifier.notifyListeners(); diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index d3e669e18..da2e48710 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -16,15 +16,15 @@ import 'package:flutter/foundation.dart'; import 'enums.dart'; mixin SourceBase { - final List _rawEntries = []; + final List _rawEntries = []; - List get rawEntries => List.unmodifiable(_rawEntries); + List get rawEntries => List.unmodifiable(_rawEntries); final EventBus _eventBus = EventBus(); EventBus get eventBus => _eventBus; - List get sortedEntriesForFilterList; + List get sortedEntriesForFilterList; final Map _filterEntryCountMap = {}; @@ -39,7 +39,7 @@ mixin SourceBase { abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { @override - List get sortedEntriesForFilterList => CollectionLens( + List get sortedEntriesForFilterList => CollectionLens( source: this, groupFactor: EntryGroupFactor.none, sortFactor: EntrySortFactor.date, @@ -55,7 +55,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${_savedDates.length} entries'); } - void addAll(Iterable entries) { + void addAll(Iterable entries) { if (_rawEntries.isNotEmpty) { final newContentIds = entries.map((entry) => entry.contentId).toList(); _rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId)); @@ -70,7 +70,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM eventBus.fire(EntryAddedEvent()); } - void removeEntries(List entries) { + void removeEntries(List entries) { entries.forEach((entry) => entry.removeFromFavourites()); _rawEntries.removeWhere(entries.contains); cleanEmptyAlbums(entries.map((entry) => entry.directory).toSet()); @@ -91,7 +91,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM // `dateModifiedSecs` changes when moving entries to another directory, // but it does not change when renaming the containing directory - Future moveEntry(ImageEntry entry, Map newFields) async { + Future moveEntry(AvesEntry entry, Map newFields) async { final oldContentId = entry.contentId; final newContentId = newFields['contentId'] as int; final newDateModifiedSecs = newFields['dateModifiedSecs'] as int; @@ -109,7 +109,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } void updateAfterMove({ - @required Set selection, + @required Set selection, @required bool copy, @required String destinationAlbum, @required Iterable movedOps, @@ -117,7 +117,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM if (movedOps.isEmpty) return; final fromAlbums = {}; - final movedEntries = []; + final movedEntries = []; if (copy) { movedOps.forEach((movedOp) { final sourceUri = movedOp.uri; @@ -166,25 +166,25 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM Future refresh(); - Future refreshMetadata(Set entries); + Future refreshMetadata(Set entries); } enum SourceState { loading, cataloguing, locating, ready } class EntryAddedEvent { - final ImageEntry entry; + final AvesEntry entry; const EntryAddedEvent([this.entry]); } class EntryRemovedEvent { - final Iterable entries; + final Iterable entries; const EntryRemovedEvent(this.entries); } class EntryMovedEvent { - final Iterable entries; + final Iterable entries; const EntryMovedEvent(this.entries); } diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index 6312bffeb..16de86443 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:aves/model/filters/location.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:collection/collection.dart'; @@ -28,7 +28,7 @@ mixin LocationMixin on SourceBase { Future locateEntries() async { // final stopwatch = Stopwatch()..start(); - final byLocated = groupBy(rawEntries.where((entry) => entry.hasGps), (entry) => entry.isLocated); + final byLocated = groupBy(rawEntries.where((entry) => entry.hasGps), (entry) => entry.isLocated); final todo = byLocated[false] ?? []; if (todo.isEmpty) return; @@ -42,7 +42,7 @@ mixin LocationMixin on SourceBase { // - 652 calls (22%) when approximating to 2 decimal places (~1km - town or village) // cf https://en.wikipedia.org/wiki/Decimal_degrees#Precision final latLngFactor = pow(10, 2); - Tuple2 approximateLatLng(ImageEntry entry) { + Tuple2 approximateLatLng(AvesEntry entry) { final lat = entry.catalogMetadata?.latitude; final lng = entry.catalogMetadata?.longitude; if (lat == null || lng == null) return null; @@ -57,7 +57,7 @@ mixin LocationMixin on SourceBase { setProgress(done: progressDone, total: progressTotal); final newAddresses = []; - await Future.forEach(todo, (entry) async { + await Future.forEach(todo, (entry) async { final latLng = approximateLatLng(entry); if (knownLocations.containsKey(latLng)) { entry.addressDetails = knownLocations[latLng]?.copyWith(contentId: entry.contentId); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index c0afae1aa..611db5974 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -1,7 +1,7 @@ import 'dart:math'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -55,14 +55,14 @@ class MediaStoreSource extends CollectionSource { // fetch new entries var refreshCount = 10; const refreshCountMax = 1000; - final allNewEntries = [], pendingNewEntries = []; + final allNewEntries = [], pendingNewEntries = []; void addPendingEntries() { allNewEntries.addAll(pendingNewEntries); addAll(pendingNewEntries); pendingNewEntries.clear(); } - ImageFileService.getImageEntries(knownEntryMap).listen( + ImageFileService.getEntries(knownEntryMap).listen( (entry) { pendingNewEntries.add(entry); if (pendingNewEntries.length >= refreshCount) { @@ -96,7 +96,7 @@ class MediaStoreSource extends CollectionSource { } @override - Future refreshMetadata(Set entries) { + Future refreshMetadata(Set entries) { final contentIds = entries.map((entry) => entry.contentId).toSet(); metadataDb.removeIds(contentIds, updateFavourites: false); return refresh(); diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 55a1053ca..d5b1ca3c0 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:collection/collection.dart'; @@ -31,7 +31,7 @@ mixin TagMixin on SourceBase { setProgress(done: progressDone, total: progressTotal); final newMetadata = []; - await Future.forEach(todo, (entry) async { + await Future.forEach(todo, (entry) async { await entry.catalog(background: true); if (entry.isCatalogued) { newMetadata.add(entry.catalogMetadata); diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 016cffb27..4f6b6750c 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -81,10 +81,10 @@ class AndroidAppService { return false; } - static Future shareEntries(Iterable entries) async { + static Future shareEntries(Iterable entries) async { // loosen mime type to a generic one, so we can share with badly defined apps // 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())); + final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); try { return await platform.invokeMethod('share', { 'title': 'Share via:', diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index 31392df08..3815c5df8 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -26,7 +26,7 @@ class AndroidDebugService { return {}; } - static Future getBitmapFactoryInfo(ImageEntry entry) async { + static Future getBitmapFactoryInfo(AvesEntry entry) async { try { // return map with all data available when decoding image bounds with `BitmapFactory` final result = await platform.invokeMethod('getBitmapFactoryInfo', { @@ -39,7 +39,7 @@ class AndroidDebugService { return {}; } - static Future getContentResolverMetadata(ImageEntry entry) async { + static Future getContentResolverMetadata(AvesEntry entry) async { try { // return map with all data available from the content resolver final result = await platform.invokeMethod('getContentResolverMetadata', { @@ -53,7 +53,7 @@ class AndroidDebugService { return {}; } - static Future getExifInterfaceMetadata(ImageEntry entry) async { + static Future getExifInterfaceMetadata(AvesEntry entry) async { try { // return map with all data available from the `ExifInterface` library final result = await platform.invokeMethod('getExifInterfaceMetadata', { @@ -68,7 +68,7 @@ class AndroidDebugService { return {}; } - static Future getMediaMetadataRetrieverMetadata(ImageEntry entry) async { + static Future getMediaMetadataRetrieverMetadata(AvesEntry entry) async { try { // return map with all data available from `MediaMetadataRetriever` final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', { @@ -81,7 +81,7 @@ class AndroidDebugService { return {}; } - static Future getMetadataExtractorSummary(ImageEntry entry) async { + static Future getMetadataExtractorSummary(AvesEntry entry) async { try { // return map with the mime type and tag count for each directory found by `metadata-extractor` final result = await platform.invokeMethod('getMetadataExtractorSummary', { @@ -96,7 +96,7 @@ class AndroidDebugService { return {}; } - static Future getTiffStructure(ImageEntry entry) async { + static Future getTiffStructure(AvesEntry entry) async { if (entry.mimeType != MimeTypes.tiff) return {}; try { diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart index 6e0bf158a..a358b5413 100644 --- a/lib/services/app_shortcut_service.dart +++ b/lib/services/app_shortcut_service.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -26,7 +26,7 @@ class AppShortcutService { return false; } - static Future pin(String label, ImageEntry entry, Set filters) async { + static Future pin(String label, AvesEntry entry, Set filters) async { Uint8List iconBytes; if (entry != null) { final size = entry.isVideo ? 0.0 : 256.0; diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 80cd3fc37..af68d8bd9 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/service_policy.dart'; import 'package:flutter/foundation.dart'; @@ -18,7 +18,7 @@ class ImageFileService { static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream'); static const double thumbnailDefaultSize = 64.0; - static Map _toPlatformEntryMap(ImageEntry entry) { + static Map _toPlatformEntryMap(AvesEntry entry) { return { 'uri': entry.uri, 'path': entry.path, @@ -32,13 +32,13 @@ class ImageFileService { } // knownEntries: map of contentId -> dateModifiedSecs - static Stream getImageEntries(Map knownEntries) { + static Stream getEntries(Map knownEntries) { try { return mediaStoreChannel.receiveBroadcastStream({ 'knownEntries': knownEntries, - }).map((event) => ImageEntry.fromMap(event)); + }).map((event) => AvesEntry.fromMap(event)); } on PlatformException catch (e) { - debugPrint('getImageEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + debugPrint('getEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}'); return Stream.error(e); } } @@ -55,16 +55,16 @@ class ImageFileService { return []; } - static Future getImageEntry(String uri, String mimeType) async { - debugPrint('getImageEntry for uri=$uri, mimeType=$mimeType'); + static Future getEntry(String uri, String mimeType) async { + debugPrint('getEntry for uri=$uri, mimeType=$mimeType'); try { - final result = await platform.invokeMethod('getImageEntry', { + final result = await platform.invokeMethod('getEntry', { 'uri': uri, 'mimeType': mimeType, }) as Map; - return ImageEntry.fromMap(result); + return AvesEntry.fromMap(result); } on PlatformException catch (e) { - debugPrint('getImageEntry failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + debugPrint('getEntry failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return null; } @@ -224,7 +224,7 @@ class ImageFileService { static Future resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); - static Stream delete(Iterable entries) { + static Stream delete(Iterable entries) { try { return opChannel.receiveBroadcastStream({ 'op': 'delete', @@ -236,7 +236,7 @@ class ImageFileService { } } - static Stream move(Iterable entries, {@required bool copy, @required String destinationAlbum}) { + static Stream move(Iterable entries, {@required bool copy, @required String destinationAlbum}) { try { return opChannel.receiveBroadcastStream({ 'op': 'move', @@ -250,7 +250,7 @@ class ImageFileService { } } - static Future rename(ImageEntry entry, String newName) async { + static Future rename(AvesEntry entry, String newName) async { try { // return map with: 'contentId' 'path' 'title' 'uri' (all optional) final result = await platform.invokeMethod('rename', { @@ -264,7 +264,7 @@ class ImageFileService { return {}; } - static Future rotate(ImageEntry entry, {@required bool clockwise}) async { + static Future rotate(AvesEntry entry, {@required bool clockwise}) async { try { // return map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('rotate', { @@ -278,7 +278,7 @@ class ImageFileService { return {}; } - static Future flip(ImageEntry entry) async { + static Future flip(AvesEntry entry) async { try { // return map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('flip', { diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index 9a53587c6..bb058efda 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/services/service_policy.dart'; @@ -12,7 +12,7 @@ class MetadataService { static const platform = MethodChannel('deckers.thibault/aves/metadata'); // return Map> (map of directories, each directory being a map of metadata label and value description) - static Future getAllMetadata(ImageEntry entry) async { + static Future getAllMetadata(AvesEntry entry) async { if (entry.isSvg) return null; try { @@ -28,7 +28,7 @@ class MetadataService { return {}; } - static Future getCatalogMetadata(ImageEntry entry, {bool background = false}) async { + static Future getCatalogMetadata(AvesEntry entry, {bool background = false}) async { if (entry.isSvg) return null; Future call() async { @@ -65,7 +65,7 @@ class MetadataService { : call(); } - static Future getOverlayMetadata(ImageEntry entry) async { + static Future getOverlayMetadata(AvesEntry entry) async { if (entry.isSvg) return null; try { @@ -82,7 +82,7 @@ class MetadataService { return null; } - static Future getMultiPageInfo(ImageEntry entry) async { + static Future getMultiPageInfo(AvesEntry entry) async { try { final result = await platform.invokeMethod('getMultiPageInfo', { 'mimeType': entry.mimeType, @@ -95,7 +95,7 @@ class MetadataService { return null; } - static Future getPanoramaInfo(ImageEntry entry) async { + static Future getPanoramaInfo(AvesEntry entry) async { try { // return map with values for: // 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int), @@ -124,7 +124,7 @@ class MetadataService { return []; } - static Future> getExifThumbnails(ImageEntry entry) async { + static Future> getExifThumbnails(AvesEntry entry) async { try { final result = await platform.invokeMethod('getExifThumbnails', { 'mimeType': entry.mimeType, @@ -138,7 +138,7 @@ class MetadataService { return []; } - static Future extractXmpDataProp(ImageEntry entry, String propPath, String propMimeType) async { + static Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType) async { try { final result = await platform.invokeMethod('extractXmpDataProp', { 'mimeType': entry.mimeType, diff --git a/lib/services/svg_metadata_service.dart b/lib/services/svg_metadata_service.dart index 1152a74d9..b5823e7ff 100644 --- a/lib/services/svg_metadata_service.dart +++ b/lib/services/svg_metadata_service.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:flutter/foundation.dart'; @@ -15,7 +15,7 @@ class SvgMetadataService { static const _textElements = ['title', 'desc']; static const _metadataElement = 'metadata'; - static Future getSize(ImageEntry entry) async { + static Future getSize(AvesEntry entry) async { try { final data = await ImageFileService.getSvg(entry.uri, entry.mimeType); @@ -48,7 +48,7 @@ class SvgMetadataService { return null; } - static Future>> getAllMetadata(ImageEntry entry) async { + static Future>> getAllMetadata(AvesEntry entry) async { String formatKey(String key) { switch (key) { case 'desc': diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 2d132398d..b1f83b45a 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:aves/model/actions/collection_actions.dart'; import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.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'; @@ -22,7 +22,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware CollectionSource get source => collection.source; - Set get selection => collection.selection; + Set get selection => collection.selection; EntrySetActionDelegate({ @required this.collection, diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart index 3dad0d735..c481e2ce1 100644 --- a/lib/widgets/collection/grid/section_layout.dart +++ b/lib/widgets/collection/grid/section_layout.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart'; @@ -6,7 +6,7 @@ import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider { +class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider { final CollectionLens collection; const SectionedEntryListLayoutProvider({ @@ -14,7 +14,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider collection.showHeaders; @override - Map> get sections => collection.sections; + Map> get sections => collection.sections; @override double getHeaderExtent(BuildContext context, SectionKey sectionKey) { diff --git a/lib/widgets/collection/grid/selector.dart b/lib/widgets/collection/grid/selector.dart index 513a7beeb..be1f48d66 100644 --- a/lib/widgets/collection/grid/selector.dart +++ b/lib/widgets/collection/grid/selector.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; @@ -39,7 +39,7 @@ class _GridSelectionGestureDetectorState extends State widget.collection; - List get entries => collection.sortedEntries; + List get entries => collection.sortedEntries; ScrollController get scrollController => widget.scrollController; @@ -131,12 +131,12 @@ class _GridSelectionGestureDetectorState extends State>(); + final sectionedListLayout = context.read>(); return sectionedListLayout.getItemAt(offset); } diff --git a/lib/widgets/collection/grid/thumbnail.dart b/lib/widgets/collection/grid/thumbnail.dart index 64866df0b..c6b6a4d75 100644 --- a/lib/widgets/collection/grid/thumbnail.dart +++ b/lib/widgets/collection/grid/thumbnail.dart @@ -1,5 +1,5 @@ import 'package:aves/main.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; class InteractiveThumbnail extends StatelessWidget { final CollectionLens collection; - final ImageEntry entry; + final AvesEntry entry; final double tileExtent; final ValueNotifier isScrollingNotifier; diff --git a/lib/widgets/collection/thumbnail/decorated.dart b/lib/widgets/collection/thumbnail/decorated.dart index 724000503..40720893f 100644 --- a/lib/widgets/collection/thumbnail/decorated.dart +++ b/lib/widgets/collection/thumbnail/decorated.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/thumbnail/overlay.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; @@ -6,7 +6,7 @@ import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:flutter/material.dart'; class DecoratedThumbnail extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final CollectionLens collection; final ValueNotifier isScrollingNotifier; diff --git a/lib/widgets/collection/thumbnail/error.dart b/lib/widgets/collection/thumbnail/error.dart index f66274fde..6b531adeb 100644 --- a/lib/widgets/collection/thumbnail/error.dart +++ b/lib/widgets/collection/thumbnail/error.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:flutter/material.dart'; class ErrorThumbnail extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final String tooltip; diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index 771faf505..4858f28b3 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/enums.dart'; @@ -14,7 +14,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class ThumbnailEntryOverlay extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; const ThumbnailEntryOverlay({ @@ -61,7 +61,7 @@ class ThumbnailEntryOverlay extends StatelessWidget { } class ThumbnailSelectionOverlay extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; const ThumbnailSelectionOverlay({ @@ -121,7 +121,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget { } class ThumbnailHighlightOverlay extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; const ThumbnailHighlightOverlay({ @@ -137,7 +137,7 @@ class ThumbnailHighlightOverlay extends StatefulWidget { class _ThumbnailHighlightOverlayState extends State { final ValueNotifier _highlightedNotifier = ValueNotifier(false); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 8ab298158..d0cfe0d24 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -1,13 +1,13 @@ import 'package:aves/image_providers/thumbnail_provider.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/error.dart'; import 'package:aves/widgets/common/fx/transition_image.dart'; import 'package:flutter/material.dart'; class RasterImageThumbnail extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final int page; final ValueNotifier isScrollingNotifier; @@ -29,7 +29,7 @@ class RasterImageThumbnail extends StatefulWidget { class _RasterImageThumbnailState extends State { ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; double get extent => widget.extent; diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index 5561f587b..1c58c72ee 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -1,5 +1,5 @@ import 'package:aves/image_providers/uri_picture_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; @@ -8,7 +8,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; class VectorImageThumbnail extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final Object heroTag; diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index 57b086c66..aa2c848c9 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -4,7 +4,7 @@ import 'package:aves/main.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/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/ref/mime_types.dart'; @@ -82,7 +82,7 @@ class ThumbnailCollection extends StatelessWidget { ), ); - final scaler = GridScaleGestureDetector( + final scaler = GridScaleGestureDetector( tileExtentManager: tileExtentManager, scrollableKey: _scrollableKey, appBarHeightNotifier: _appBarHeightNotifier, @@ -106,7 +106,7 @@ class ThumbnailCollection extends StatelessWidget { highlightable: false, ), getScaledItemTileRect: (context, entry) { - final sectionedListLayout = context.read>(); + final sectionedListLayout = context.read>(); return sectionedListLayout.getTileRect(entry) ?? Rect.zero; }, onScaled: (entry) => Provider.of(context, listen: false).add(entry), @@ -225,7 +225,7 @@ class _CollectionScrollViewState extends State { child: _buildEmptyCollectionPlaceholder(collection), hasScrollBody: false, ) - : SectionedListSliver(), + : SectionedListSliver(), BottomPaddingSliver(), ], ); diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index ae69e3a7b..45e02fbed 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:flushbar/flushbar.dart'; @@ -31,7 +31,7 @@ mixin FeedbackMixin { void showOpReport({ @required BuildContext context, - @required Set selection, + @required Set selection, @required Stream opStream, @required void Function(Set processed) onDone, }) { diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index 58b896830..5d3ac5255 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -1,10 +1,10 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; mixin PermissionAwareMixin { - Future checkStoragePermission(BuildContext context, Set entries) { + Future checkStoragePermission(BuildContext context, Set entries) { return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).toSet()); } diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index d3d7bd18c..73ef17084 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; mixin SizeAwareMixin { - Future checkFreeSpaceForMove(BuildContext context, Set selection, String destinationAlbum, bool copy) async { + Future checkFreeSpaceForMove(BuildContext context, Set selection, String destinationAlbum, bool copy) async { final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); final free = await AndroidFileService.getFreeSpace(destinationVolume); int needed; @@ -20,7 +20,7 @@ mixin SizeAwareMixin { needed = selection.fold(0, sumSize); } else { // 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 = groupBy(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)); final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); 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 diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index adbf3fa8e..4fd080246 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:aves/image_providers/app_icon_image_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; @@ -9,7 +9,7 @@ import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; class VideoIcon extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double iconSize; final bool showDuration; diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 09626c721..4d560ceb8 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -26,7 +26,7 @@ class AppDebugPage extends StatefulWidget { } class _AppDebugPageState extends State { - List get entries => widget.source.rawEntries; + List get entries => widget.source.rawEntries; static OverlayEntry _taskQueueOverlayEntry; diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index bcf80693b..1e261e43f 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -1,6 +1,6 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -13,7 +13,7 @@ class DebugAppDatabaseSection extends StatefulWidget { class _DebugAppDatabaseSectionState extends State with AutomaticKeepAliveClientMixin { Future _dbFileSizeLoader; - Future> _dbEntryLoader; + Future> _dbEntryLoader; Future> _dbDateLoader; Future> _dbMetadataLoader; Future> _dbAddressLoader; diff --git a/lib/widgets/dialogs/rename_entry_dialog.dart b/lib/widgets/dialogs/rename_entry_dialog.dart index b28e25730..d73410ba9 100644 --- a/lib/widgets/dialogs/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/rename_entry_dialog.dart @@ -1,13 +1,13 @@ import 'dart:io'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; import 'aves_dialog.dart'; class RenameEntryDialog extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; const RenameEntryDialog(this.entry); @@ -19,7 +19,7 @@ class _RenameEntryDialogState extends State { final TextEditingController _nameController = TextEditingController(); final ValueNotifier _isValidNotifier = ValueNotifier(false); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 5842967ee..1801a2454 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; class DecoratedFilterChip extends StatelessWidget { final CollectionSource source; final CollectionFilter filter; - final ImageEntry entry; + final AvesEntry entry; final double extent; final bool pinned, highlightable; final FilterCallback onTap; diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index d75f9c6dd..5d56a05b5 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -1,6 +1,6 @@ import 'package:aves/main.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; @@ -35,7 +35,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { MediaStoreSource _mediaStore; - ImageEntry _viewerEntry; + AvesEntry _viewerEntry; String _shortcutRouteName; List _shortcutFilters; @@ -108,8 +108,8 @@ class _HomePageState extends State { unawaited(Navigator.pushReplacement(context, _getRedirectRoute())); } - Future _initViewerEntry({@required String uri, @required String mimeType}) async { - final entry = await ImageFileService.getImageEntry(uri, mimeType); + Future _initViewerEntry({@required String uri, @required String mimeType}) async { + final entry = await ImageFileService.getEntry(uri, mimeType); if (entry != null) { // cataloguing is essential for geolocation and video rotation await entry.catalog(); diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index 2907bcb96..0292f1a0a 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -4,7 +4,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -30,7 +30,7 @@ class StatsPage extends StatelessWidget { final CollectionLens parentCollection; final Map entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {}; - List get entries => parentCollection?.sortedEntries ?? source.rawEntries; + List get entries => parentCollection?.sortedEntries ?? source.rawEntries; static const mimeDonutMinWidth = 124.0; @@ -66,7 +66,7 @@ class StatsPage extends StatelessWidget { text: 'No images', ); } else { - final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); + final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image/'))); final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video/'))); final mimeDonuts = Wrap( diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index 641ee2709..ad5df9358 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -1,11 +1,11 @@ -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; class DbTab extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; const DbTab({@required this.entry}); @@ -15,11 +15,11 @@ class DbTab extends StatefulWidget { class _DbTabState extends State { Future _dbDateLoader; - Future _dbEntryLoader; + Future _dbEntryLoader; Future _dbMetadataLoader; Future _dbAddressLoader; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { @@ -60,7 +60,7 @@ class _DbTabState extends State { }, ), SizedBox(height: 16), - FutureBuilder( + FutureBuilder( future: _dbEntryLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); diff --git a/lib/widgets/viewer/debug/metadata.dart b/lib/widgets/viewer/debug/metadata.dart index 075cde487..63bb3b178 100644 --- a/lib/widgets/viewer/debug/metadata.dart +++ b/lib/widgets/viewer/debug/metadata.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/android_debug_service.dart'; import 'package:aves/utils/constants.dart'; @@ -10,7 +10,7 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; class MetadataTab extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; const MetadataTab({@required this.entry}); @@ -25,7 +25,7 @@ class _MetadataTabState extends State { static const secondTimestampKeys = ['date_added', 'date_modified', 'date_expires', 'isPlayed']; static const millisecondTimestampKeys = ['datetaken', 'datetime']; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index 768a2883c..c18f3d9a2 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -1,7 +1,7 @@ import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/main.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/viewer/debug/db.dart'; import 'package:aves/widgets/viewer/debug/metadata.dart'; @@ -13,7 +13,7 @@ import 'package:tuple/tuple.dart'; class ViewerDebugPage extends StatelessWidget { static const routeName = '/viewer/debug'; - final ImageEntry entry; + final AvesEntry entry; const ViewerDebugPage({@required this.entry}); diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index 8aea86ac8..b2f298c8b 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; @@ -28,7 +28,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { bool get hasCollection => collection != null; - void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) { + void onActionSelected(BuildContext context, AvesEntry entry, EntryAction action) { switch (action) { case EntryAction.toggleFavourite: entry.toggleFavourite(); @@ -88,21 +88,21 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { } } - Future _flip(BuildContext context, ImageEntry entry) async { + Future _flip(BuildContext context, AvesEntry entry) async { if (!await checkStoragePermission(context, {entry})) return; final success = await entry.flip(); if (!success) showFeedback(context, 'Failed'); } - Future _rotate(BuildContext context, ImageEntry 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); if (!success) showFeedback(context, 'Failed'); } - Future _showDeleteDialog(BuildContext context, ImageEntry entry) async { + Future _showDeleteDialog(BuildContext context, AvesEntry entry) async { final confirmed = await showDialog( context: context, builder: (context) { @@ -140,7 +140,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { } } - Future _showRenameDialog(BuildContext context, ImageEntry entry) async { + Future _showRenameDialog(BuildContext context, AvesEntry entry) async { final newName = await showDialog( context: context, builder: (context) => RenameEntryDialog(entry), @@ -152,7 +152,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { showFeedback(context, await entry.rename(newName) ? 'Done!' : 'Failed'); } - void _goToSourceViewer(BuildContext context, ImageEntry entry) { + void _goToSourceViewer(BuildContext context, AvesEntry entry) { Navigator.push( context, MaterialPageRoute( @@ -164,7 +164,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { ); } - void _goToDebug(BuildContext context, ImageEntry entry) { + void _goToDebug(BuildContext context, AvesEntry entry) { Navigator.push( context, MaterialPageRoute( diff --git a/lib/widgets/viewer/entry_scroller.dart b/lib/widgets/viewer/entry_scroller.dart index baa4bee88..d4ec97c0e 100644 --- a/lib/widgets/viewer/entry_scroller.dart +++ b/lib/widgets/viewer/entry_scroller.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart'; @@ -34,7 +34,7 @@ class MultiEntryScroller extends StatefulWidget { } class _MultiEntryScrollerState extends State with AutomaticKeepAliveClientMixin { - List get entries => widget.collection.sortedEntries; + List get entries => widget.collection.sortedEntries; @override Widget build(BuildContext context) { @@ -80,7 +80,7 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - Widget _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page}) { + Widget _buildViewer(AvesEntry entry, {MultiPageInfo multiPageInfo, int page}) { return Selector( selector: (c, mq) => mq.size, builder: (c, mqSize, child) { @@ -99,7 +99,7 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - MultiPageController _getMultiPageController(ImageEntry entry) { + MultiPageController _getMultiPageController(AvesEntry entry) { return widget.multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; } @@ -108,7 +108,7 @@ class _MultiEntryScrollerState extends State with AutomaticK } class SingleEntryScroller extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final VoidCallback onTap; final List> videoControllers; final List> multiPageControllers; @@ -125,7 +125,7 @@ class SingleEntryScroller extends StatefulWidget { } class _SingleEntryScrollerState extends State with AutomaticKeepAliveClientMixin { - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override Widget build(BuildContext context) { @@ -173,7 +173,7 @@ class _SingleEntryScrollerState extends State with Automati ); } - MultiPageController _getMultiPageController(ImageEntry entry) { + MultiPageController _getMultiPageController(AvesEntry entry) { return widget.multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; } diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 65d35279b..5ee08cdd0 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; @@ -8,7 +8,7 @@ class MultiEntryViewerPage extends AnimatedWidget { static const routeName = '/viewer'; final CollectionLens collection; - final ImageEntry initialEntry; + final AvesEntry initialEntry; const MultiEntryViewerPage({ Key key, @@ -34,7 +34,7 @@ class MultiEntryViewerPage extends AnimatedWidget { class SingleEntryViewerPage extends StatelessWidget { static const routeName = '/viewer'; - final ImageEntry entry; + final AvesEntry entry; const SingleEntryViewerPage({ Key key, diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 584f3d6c4..4ff471002 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -31,7 +31,7 @@ import 'package:tuple/tuple.dart'; class EntryViewerStack extends StatefulWidget { final CollectionLens collection; - final ImageEntry initialEntry; + final AvesEntry initialEntry; const EntryViewerStack({ Key key, @@ -44,7 +44,7 @@ class EntryViewerStack extends StatefulWidget { } class _EntryViewerStackState extends State with SingleTickerProviderStateMixin, WidgetsBindingObserver { - final ValueNotifier _entryNotifier = ValueNotifier(null); + final ValueNotifier _entryNotifier = ValueNotifier(null); int _currentHorizontalPage; ValueNotifier _currentVerticalPage; PageController _horizontalPager, _verticalPager; @@ -63,7 +63,7 @@ class _EntryViewerStackState extends State with SingleTickerPr 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; @@ -199,7 +199,7 @@ class _EntryViewerStackState extends State with SingleTickerPr } Widget _buildTopOverlay() { - final child = ValueListenableBuilder( + final child = ValueListenableBuilder( valueListenable: _entryNotifier, builder: (context, entry, child) { if (entry == null) return SizedBox.shrink(); @@ -232,7 +232,7 @@ class _EntryViewerStackState extends State with SingleTickerPr } Widget _buildBottomOverlay() { - Widget bottomOverlay = ValueListenableBuilder( + Widget bottomOverlay = ValueListenableBuilder( valueListenable: _entryNotifier, builder: (context, entry, child) { if (entry == null) return SizedBox.shrink(); @@ -312,7 +312,7 @@ class _EntryViewerStackState extends State with SingleTickerPr return bottomOverlay; } - MultiPageController _getMultiPageController(ImageEntry entry) { + MultiPageController _getMultiPageController(AvesEntry entry) { return entry.isMultipage ? _multiPageControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2 : null; } @@ -478,7 +478,7 @@ class _EntryViewerStackState extends State with SingleTickerPr class ViewerVerticalPageView extends StatefulWidget { final CollectionLens collection; - final ValueNotifier entryNotifier; + final ValueNotifier entryNotifier; final List> videoControllers; final List> multiPageControllers; final PageController horizontalPager, verticalPager; @@ -507,13 +507,13 @@ class ViewerVerticalPageView extends StatefulWidget { class _ViewerVerticalPageViewState extends State { final ValueNotifier _backgroundColorNotifier = ValueNotifier(Colors.black); final ValueNotifier _infoPageVisibleNotifier = ValueNotifier(false); - ImageEntry _oldEntry; + AvesEntry _oldEntry; CollectionLens get collection => widget.collection; bool get hasCollection => collection != null; - ImageEntry get entry => widget.entryNotifier.value; + AvesEntry get entry => widget.entryNotifier.value; @override void initState() { diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index db19c30c4..26cedfa6a 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -3,7 +3,7 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/utils/constants.dart'; @@ -15,7 +15,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class BasicSection extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final CollectionLens collection; final FilterCallback onFilter; diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index df0eee399..34e2aebe2 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/viewer/info/info_search.dart'; @@ -6,7 +6,7 @@ import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:flutter/material.dart'; class InfoAppBar extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier> metadataNotifier; final VoidCallback onBackPressed; diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index fae51760e..87b9afe34 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -1,5 +1,5 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; +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/basic/insets.dart'; @@ -14,7 +14,7 @@ import 'package:provider/provider.dart'; class InfoPage extends StatefulWidget { final CollectionLens collection; - final ValueNotifier entryNotifier; + final ValueNotifier entryNotifier; final ValueNotifier visibleNotifier; const InfoPage({ @@ -34,7 +34,7 @@ class _InfoPageState extends State { CollectionLens get collection => widget.collection; - ImageEntry get entry => widget.entryNotifier.value; + AvesEntry get entry => widget.entryNotifier.value; @override Widget build(BuildContext context) { @@ -48,7 +48,7 @@ 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 @@ -107,7 +107,7 @@ class _InfoPageState extends State { class _InfoPageContent extends StatefulWidget { final CollectionLens collection; - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier visibleNotifier; final ScrollController scrollController; final bool split; @@ -134,7 +134,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { CollectionLens get collection => widget.collection; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index 0e12b4276..04744e277 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; @@ -7,7 +7,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class InfoSearchDelegate extends SearchDelegate { - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier> metadataNotifier; Map get metadata => metadataNotifier.value; diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 128bdf262..bbf521cc2 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -1,5 +1,5 @@ import 'package:aves/model/filters/location.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; @@ -17,7 +17,7 @@ import 'package:tuple/tuple.dart'; class LocationSection extends StatefulWidget { final CollectionLens collection; - final ImageEntry entry; + final AvesEntry entry; final bool showTitle; final ValueNotifier visibleNotifier; final FilterCallback onFilter; @@ -43,7 +43,7 @@ class _LocationSectionState extends State with TickerProviderSt CollectionLens get collection => widget.collection; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { @@ -158,7 +158,7 @@ class _LocationSectionState extends State with TickerProviderSt } class _AddressInfoGroup extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; const _AddressInfoGroup({@required this.entry}); @@ -169,7 +169,7 @@ class _AddressInfoGroup extends StatefulWidget { class _AddressInfoGroupState extends State<_AddressInfoGroup> { Future _addressLineLoader; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { diff --git a/lib/widgets/viewer/info/maps/marker.dart b/lib/widgets/viewer/info/maps/marker.dart index 0cd8bc006..c40806c22 100644 --- a/lib/widgets/viewer/info/maps/marker.dart +++ b/lib/widgets/viewer/info/maps/marker.dart @@ -1,14 +1,14 @@ import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class ImageMarker extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final double extent; final Size pointerSize; diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index e84a655d6..3bc2f74b6 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/services/svg_metadata_service.dart'; import 'package:aves/theme/icons.dart'; @@ -16,7 +16,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class MetadataDirTile extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final String title; final MetadataDirectory dir; final ValueNotifier expandedDirectoryNotifier; diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 6cba39d94..d148be209 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/services/svg_metadata_service.dart'; import 'package:aves/theme/durations.dart'; @@ -13,7 +13,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; class MetadataSectionSliver extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier visibleNotifier; final ValueNotifier> metadataNotifier; @@ -31,7 +31,7 @@ class _MetadataSectionSliverState extends State with Auto final ValueNotifier _loadedMetadataUri = ValueNotifier(null); final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; bool get isVisible => widget.visibleNotifier.value; diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 7072ac53e..646bf9736 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:flutter/material.dart'; @@ -9,7 +9,7 @@ enum MetadataThumbnailSource { embedded, exif } class MetadataThumbnails extends StatefulWidget { final MetadataThumbnailSource source; - final ImageEntry entry; + final AvesEntry entry; const MetadataThumbnails({ Key key, @@ -24,7 +24,7 @@ class MetadataThumbnails extends StatefulWidget { class _MetadataThumbnailsState extends State { Future> _loader; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; String get uri => entry.uri; diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index cbd1c5c1a..62098f1f3 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/xmp.dart'; import 'package:aves/services/android_app_service.dart'; @@ -22,7 +22,7 @@ import 'package:flutter/material.dart'; import 'package:pedantic/pedantic.dart'; class XmpDirTile extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final SplayTreeMap tags; final ValueNotifier expandedNotifier; final bool initiallyExpanded; @@ -39,7 +39,7 @@ class XmpDirTile extends StatefulWidget { } class _XmpDirTileState extends State with FeedbackMixin { - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override Widget build(BuildContext context) { @@ -123,7 +123,7 @@ class _XmpDirTileState extends State with FeedbackMixin { return; } - final embedEntry = ImageEntry.fromMap(fields); + final embedEntry = AvesEntry.fromMap(fields); unawaited(Navigator.push( context, TransparentMaterialPageRoute( diff --git a/lib/widgets/viewer/multipage.dart b/lib/widgets/viewer/multipage.dart index 004ff7a49..7bc74b9ac 100644 --- a/lib/widgets/viewer/multipage.dart +++ b/lib/widgets/viewer/multipage.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:flutter/foundation.dart'; @@ -10,7 +10,7 @@ class MultiPageController extends ChangeNotifier { final Future info; final ValueNotifier pageNotifier = ValueNotifier(0); - MultiPageController(ImageEntry entry) : info = MetadataService.getMultiPageInfo(entry); + MultiPageController(AvesEntry entry) : info = MetadataService.getMultiPageInfo(entry); int get page => pageNotifier.value; diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index d6ba71003..24e7bd2fe 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/metadata.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; @@ -20,7 +20,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class ViewerBottomOverlay extends StatefulWidget { - final List entries; + final List entries; final int index; final bool showPosition; final EdgeInsets viewInsets, viewPadding; @@ -42,10 +42,10 @@ class ViewerBottomOverlay extends StatefulWidget { class _ViewerBottomOverlayState extends State { Future _detailLoader; - ImageEntry _lastEntry; + AvesEntry _lastEntry; OverlayMetadata _lastDetails; - ImageEntry get entry { + AvesEntry get entry { final entries = widget.entries; final index = widget.index; return index < entries.length ? entries[index] : null; @@ -138,7 +138,7 @@ const double _interRowPadding = 2.0; const double _subRowMinWidth = 300.0; class _BottomOverlayContent extends AnimatedWidget { - final ImageEntry mainEntry, entry; + final AvesEntry mainEntry, entry; final MultiPageInfo multiPageInfo; final int page; final OverlayMetadata details; @@ -302,7 +302,7 @@ class _BottomOverlayContent extends AnimatedWidget { } class _LocationRow extends AnimatedWidget { - final ImageEntry entry; + final AvesEntry entry; _LocationRow({ Key key, @@ -328,7 +328,7 @@ class _LocationRow extends AnimatedWidget { } class _PositionTitleRow extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final String collectionPosition; final MultiPageController multiPageController; @@ -369,7 +369,7 @@ class _PositionTitleRow extends StatelessWidget { } class _DateRow extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final MultiPageController multiPageController; const _DateRow({ diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index 0e1a3a808..657aff590 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class Minimap extends StatelessWidget { - final ImageEntry mainEntry; + final AvesEntry mainEntry; final ValueNotifier viewStateNotifier; final MultiPageController multiPageController; final Size size; diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index a3ae822d6..b7ac56f15 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/overlay.dart'; @@ -10,7 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class MultiPageOverlay extends StatefulWidget { - final ImageEntry mainEntry; + final AvesEntry mainEntry; final MultiPageController controller; final double availableWidth; @@ -34,7 +34,7 @@ class _MultiPageOverlayState extends State { static const double extent = 48; static const double separatorWidth = 2; - ImageEntry get mainEntry => widget.mainEntry; + AvesEntry get mainEntry => widget.mainEntry; MultiPageController get controller => widget.controller; @@ -162,7 +162,7 @@ class _MultiPageOverlayState extends State { ); } - Widget _buildPageThumbnail(ImageEntry entry) { + Widget _buildPageThumbnail(AvesEntry entry) { Widget child = RasterImageThumbnail( entry: entry, extent: extent, diff --git a/lib/widgets/viewer/overlay/panorama.dart b/lib/widgets/viewer/overlay/panorama.dart index 9356a8206..0d142edeb 100644 --- a/lib/widgets/viewer/overlay/panorama.dart +++ b/lib/widgets/viewer/overlay/panorama.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/panorama_page.dart'; @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:pedantic/pedantic.dart'; class PanoramaOverlay extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final Animation scale; const PanoramaOverlay({ diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 22815ea56..e2a026346 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/favourite_repo.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -19,7 +19,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class ViewerTopOverlay extends StatelessWidget { - final ImageEntry entry; + final AvesEntry entry; final Animation scale; final EdgeInsets viewInsets, viewPadding; final Function(EntryAction value) onActionSelected; @@ -135,7 +135,7 @@ class _TopOverlayRow extends StatelessWidget { final List inAppActions; final List externalAppActions; final Animation scale; - final ImageEntry entry; + final AvesEntry entry; final Function(EntryAction value) onActionSelected; const _TopOverlayRow({ @@ -299,7 +299,7 @@ class _TopOverlayRow extends StatelessWidget { } class _FavouriteToggler extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final bool isMenuItem; final VoidCallback onPressed; diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index 1d3b287b5..3b0951e9f 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; class VideoControlOverlay extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final IjkMediaController controller; final Animation scale; @@ -37,7 +37,7 @@ class _VideoControlOverlayState extends State with SingleTi // video info is not refreshed by default, so we use a timer to do so Timer _progressTimer; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; Animation get scale => widget.scale; diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 31eeea732..979971d58 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -1,5 +1,5 @@ import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/insets.dart'; @@ -15,7 +15,7 @@ import 'package:provider/provider.dart'; class PanoramaPage extends StatefulWidget { static const routeName = '/viewer/panorama'; - final ImageEntry entry; + final AvesEntry entry; final PanoramaInfo info; const PanoramaPage({ @@ -31,7 +31,7 @@ class _PanoramaPageState extends State { final ValueNotifier _overlayVisible = ValueNotifier(true); final ValueNotifier _sensorControl = ValueNotifier(SensorControl.None); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; PanoramaInfo get info => widget.info; diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index c43028cfd..d3ab1a35c 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -1,7 +1,7 @@ import 'dart:convert'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:pdf/widgets.dart' as pdf; @@ -9,7 +9,7 @@ import 'package:pedantic/pedantic.dart'; import 'package:printing/printing.dart'; class EntryPrinter { - final ImageEntry entry; + final AvesEntry entry; const EntryPrinter(this.entry); @@ -58,7 +58,7 @@ class EntryPrinter { return pages; } - Future _buildPageImage(ImageEntry entry) async { + Future _buildPageImage(AvesEntry entry) async { if (entry.isSvg) { final bytes = await ImageFileService.getSvg(entry.uri, entry.mimeType); if (bytes != null && bytes.isNotEmpty) { diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 2e376a374..fcc8f4e70 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:aves/image_providers/uri_picture_provider.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; @@ -23,7 +23,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:tuple/tuple.dart'; class EntryPageView extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final MultiPageInfo multiPageInfo; final int page; final Size viewportSize; @@ -36,7 +36,7 @@ class EntryPageView extends StatefulWidget { EntryPageView({ Key key, - ImageEntry mainEntry, + AvesEntry mainEntry, this.multiPageInfo, this.page, this.viewportSize, @@ -56,7 +56,7 @@ class _EntryPageViewState extends State { final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); final List _subscriptions = []; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; Size get viewportSize => widget.viewportSize; diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 36124bcef..3bc3153e7 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; @@ -15,7 +15,7 @@ import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; class RasterImageView extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final ValueNotifier viewStateNotifier; final ImageErrorWidgetBuilder errorBuilder; @@ -39,7 +39,7 @@ class _RasterImageViewState extends State { ImageStreamListener _fullImageListener; final ValueNotifier _fullImageLoaded = ValueNotifier(false); - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; ValueNotifier get viewStateNotifier => widget.viewStateNotifier; @@ -323,7 +323,7 @@ class _RasterImageViewState extends State { } class RegionTile extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; // `tileRect` uses Flutter view coordinates // `regionRect` uses the raw image pixel coordinates @@ -354,7 +354,7 @@ class RegionTile extends StatefulWidget { class _RegionTileState extends State { RegionProvider _provider; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; @override void initState() { diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 28e5937e4..4cb1066a8 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'dart:ui'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; class VideoView extends StatefulWidget { - final ImageEntry entry; + final AvesEntry entry; final IjkMediaController controller; const VideoView({ @@ -23,7 +23,7 @@ class VideoView extends StatefulWidget { class _VideoViewState extends State { final List _subscriptions = []; - ImageEntry get entry => widget.entry; + AvesEntry get entry => widget.entry; IjkMediaController get controller => widget.controller; From bb08f3dcb6d9386453371043b338d639c0f3620e Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 21 Jan 2021 15:08:19 +0900 Subject: [PATCH 15/44] video: sized thumbnails match content resolver ones --- .../aves/decoder/VideoThumbnailGlideModule.kt | 28 +++++++++++++++++-- lib/widgets/collection/thumbnail/raster.dart | 4 +-- lib/widgets/viewer/visual/video.dart | 4 ++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt index 7a39b7b0a..278e55cc6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt @@ -1,7 +1,9 @@ package deckers.thibault.aves.decoder import android.content.Context +import android.media.MediaMetadataRetriever import android.net.Uri +import android.os.Build import com.bumptech.glide.Glide import com.bumptech.glide.Priority import com.bumptech.glide.Registry @@ -48,9 +50,29 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe val retriever = openMetadataRetriever(model.context, model.uri) if (retriever != null) { try { - val picture = retriever.embeddedPicture ?: retriever.frameAtTime?.getBytes(canHaveAlpha = false, recycle = false) - if (picture != null) { - callback.onDataReady(ByteArrayInputStream(picture)) + var bytes = retriever.embeddedPicture + if (bytes == null) { + // try to match the thumbnails returned by the content resolver / Media Store + // the following strategies are from empirical evidence from a few test devices: + // - API 29: sync frame closest to the middle + // - API 26/27: default representative frame at any time position + var timeMillis: Long? = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() + if (durationMillis != null) { + timeMillis = durationMillis / 2 + } + } + val frame = if (timeMillis != null) { + retriever.getFrameAtTime(timeMillis * 1000) + } else { + retriever.frameAtTime + } + bytes = frame?.getBytes(canHaveAlpha = false, recycle = false) + } + + if (bytes != null) { + callback.onDataReady(ByteArrayInputStream(bytes)) } else { callback.onLoadFailed(Exception("failed to get embedded picture or any frame")) } diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index d0cfe0d24..dcfb8a786 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -70,9 +70,7 @@ class _RasterImageThumbnailState extends State { if (!entry.canDecode) return; _fastThumbnailProvider = entry.getThumbnail(); - if (!entry.isVideo) { - _sizedThumbnailProvider = entry.getThumbnail(extent: extent); - } + _sizedThumbnailProvider = entry.getThumbnail(extent: extent); } void _pauseProvider() { diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 4cb1066a8..aac61c5e4 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -3,6 +3,8 @@ import 'dart:ui'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; @@ -98,7 +100,7 @@ class _VideoViewState extends State { backgroundColor: Colors.transparent, ) : Image( - image: entry.getBestThumbnail(entry.displaySize.longestSide), + image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)), fit: BoxFit.contain, ); }); From 5cd34da0c26d03a53a7ae53295dad013f82a646f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 21 Jan 2021 15:42:35 +0900 Subject: [PATCH 16/44] viewer: prevent raster decoration background bleed --- lib/widgets/viewer/visual/raster.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 3bc3153e7..477ce2ad5 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -185,7 +185,8 @@ class _RasterImageViewState extends State { final viewSize = _displaySize * viewState.scale; final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position; - final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source; + // deflate as a quick way to prevent background bleed + final decorationSize = (applyBoxFit(BoxFit.none, viewSize, viewportSize).source - Offset(.5, .5)) as Size; Widget child; final background = settings.rasterBackground; From 07de79fe844d9737be69c05cd8938243b5dd0346 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 21 Jan 2021 16:12:58 +0900 Subject: [PATCH 17/44] viewer: reuse decorated thumbnails for page scroller --- lib/widgets/viewer/overlay/multipage.dart | 39 ++++------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index b7ac56f15..b379a350e 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -3,8 +3,7 @@ import 'dart:math'; import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/collection/thumbnail/overlay.dart'; -import 'package:aves/widgets/collection/thumbnail/raster.dart'; +import 'package:aves/widgets/collection/thumbnail/decorated.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -122,7 +121,12 @@ class _MultiPageOverlayState extends State { ); _syncScroll = true; }, - child: _buildPageThumbnail(pageEntry), + child: DecoratedThumbnail( + entry: pageEntry, + extent: extent, + selectable: false, + highlightable: false, + ), ); }, separatorBuilder: (context, index) => separator, @@ -162,35 +166,6 @@ class _MultiPageOverlayState extends State { ); } - Widget _buildPageThumbnail(AvesEntry entry) { - Widget child = RasterImageThumbnail( - entry: entry, - extent: extent, - page: entry.page, - ); - - child = Stack( - alignment: Alignment.center, - children: [ - child, - Positioned( - bottom: 0, - left: 0, - child: ThumbnailEntryOverlay( - entry: entry, - extent: extent, - ), - ), - ], - ); - - return Container( - width: extent, - height: extent, - child: child, - ); - } - void _onScrollChange() { if (_syncScroll) { controller.page = scrollOffsetToPage(_scrollController.offset); From 60243a20fd8495c57536ba671d88e566383c8bb8 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 21 Jan 2021 17:09:25 +0900 Subject: [PATCH 18/44] improved checkered background performance --- lib/widgets/collection/thumbnail/vector.dart | 4 +- .../common/fx/checkered_decoration.dart | 55 +++++++------------ lib/widgets/settings/entry_background.dart | 4 +- lib/widgets/viewer/visual/raster.dart | 6 +- lib/widgets/viewer/visual/vector.dart | 4 +- 5 files changed, 28 insertions(+), 45 deletions(-) diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index 1c58c72ee..c930d9afa 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -31,8 +31,8 @@ class VectorImageThumbnail extends StatelessWidget { final availableSize = constraints.biggest; final fitSize = applyBoxFit(fit, entry.displaySize, availableSize).destination; final offset = fitSize / 2 - availableSize / 2; - final child = DecoratedBox( - decoration: CheckeredDecoration(checkSize: extent / 8, offset: offset), + final child = CustomPaint( + painter: CheckeredPainter(checkSize: extent / 8, offset: offset), child: SvgPicture( UriPicture( uri: entry.uri, diff --git a/lib/widgets/common/fx/checkered_decoration.dart b/lib/widgets/common/fx/checkered_decoration.dart index 4d541eaee..c5bc26520 100644 --- a/lib/widgets/common/fx/checkered_decoration.dart +++ b/lib/widgets/common/fx/checkered_decoration.dart @@ -1,57 +1,40 @@ import 'package:flutter/material.dart'; -class CheckeredDecoration extends Decoration { - final Color light, dark; +class CheckeredPainter extends CustomPainter { + final Paint lightPaint, darkPaint; final double checkSize; final Offset offset; - const CheckeredDecoration({ - this.light = const Color(0xFF999999), - this.dark = const Color(0xFF666666), + CheckeredPainter({ + Color light = const Color(0xFF999999), + Color dark = const Color(0xFF666666), this.checkSize = 20, this.offset = Offset.zero, - }); + }) : lightPaint = Paint()..color = light, + darkPaint = Paint()..color = dark; @override - _CheckeredDecorationPainter createBoxPainter([VoidCallback onChanged]) { - return _CheckeredDecorationPainter(this, onChanged); - } -} + void paint(Canvas canvas, Size size) { + final background = Rect.fromLTWH(0, 0, size.width, size.height); + canvas.drawRect(background, lightPaint); -class _CheckeredDecorationPainter extends BoxPainter { - final CheckeredDecoration decoration; - - const _CheckeredDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged); - - @override - void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { - final size = configuration.size; - var dx = offset.dx; - var dy = offset.dy; - - final lightPaint = Paint()..color = decoration.light; - final darkPaint = Paint()..color = decoration.dark; - final checkSize = decoration.checkSize; - - // save/restore because of the clip - canvas.save(); - canvas.clipRect(Rect.fromLTWH(dx, dy, size.width, size.height)); - - canvas.drawPaint(lightPaint); - - dx += decoration.offset.dx % (decoration.checkSize * 2); - dy += decoration.offset.dy % (decoration.checkSize * 2); + final dx = offset.dx % (checkSize * 2); + final dy = offset.dy % (checkSize * 2); final xMax = size.width / checkSize; final yMax = size.height / checkSize; for (var x = -2; x < xMax; x++) { for (var y = -2; y < yMax; y++) { if ((x + y) % 2 == 0) { - final rect = Rect.fromLTWH(dx + x * checkSize, dy + y * checkSize, checkSize, checkSize); - canvas.drawRect(rect, darkPaint); + final check = Rect.fromLTWH(dx + x * checkSize, dy + y * checkSize, checkSize, checkSize); + if (check.overlaps(background)) { + canvas.drawRect(check.intersect(background), darkPaint); + } } } } - canvas.restore(); } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; } diff --git a/lib/widgets/settings/entry_background.dart b/lib/widgets/settings/entry_background.dart index ade54d894..51b0a9326 100644 --- a/lib/widgets/settings/entry_background.dart +++ b/lib/widgets/settings/entry_background.dart @@ -50,8 +50,8 @@ class _EntryBackgroundSelectorState extends State { break; case EntryBackground.checkered: child = ClipOval( - child: DecoratedBox( - decoration: CheckeredDecoration( + child: CustomPaint( + painter: CheckeredPainter( checkSize: radius, ), ), diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 477ce2ad5..72098fb99 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:aves/image_providers/region_provider.dart'; -import 'package:aves/model/entry_images.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/settings.dart'; import 'package:aves/utils/math_utils.dart'; @@ -199,8 +199,8 @@ class _RasterImageViewState extends State { builder: (context, fullImageLoaded, child) { if (!fullImageLoaded) return SizedBox.shrink(); - return DecoratedBox( - decoration: CheckeredDecoration( + return CustomPaint( + painter: CheckeredPainter( checkSize: checkSize, offset: offset, ), diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index 443b01602..1fe9d3e28 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -36,8 +36,8 @@ class VectorViewCheckeredBackground extends StatelessWidget { Positioned( width: decorationSize.width, height: decorationSize.height, - child: DecoratedBox( - decoration: CheckeredDecoration( + child: CustomPaint( + painter: CheckeredPainter( checkSize: checkSize, offset: offset, ), From a6b99e7c2a7be787f0fc8bebf912f47c6172c6ed Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 22 Jan 2021 13:42:17 +0900 Subject: [PATCH 19/44] multipage: open with default track --- .../aves/channel/calls/ImageFileHandler.kt | 8 ++-- .../aves/channel/calls/MetadataHandler.kt | 19 ++++++--- .../aves/channel/calls/ThumbnailFetcher.kt | 16 ++++---- .../channel/streams/ImageByteStreamHandler.kt | 12 +++--- .../decoder/MultiTrackImageGlideModule.kt | 6 +-- .../thibault/aves/metadata/MultiTrackMedia.kt | 31 ++++++++------- lib/image_providers/region_provider.dart | 18 ++++----- lib/image_providers/thumbnail_provider.dart | 18 ++++----- lib/image_providers/uri_image_provider.dart | 16 ++++---- lib/model/entry.dart | 16 +++----- lib/model/entry_cache.dart | 10 ++--- lib/model/entry_images.dart | 6 +-- lib/model/multipage.dart | 39 +++++++++++-------- lib/services/app_shortcut_service.dart | 2 +- lib/services/image_file_service.dart | 12 +++--- lib/services/metadata_service.dart | 5 ++- lib/widgets/collection/thumbnail/raster.dart | 2 - lib/widgets/viewer/entry_scroller.dart | 10 ++--- lib/widgets/viewer/multipage.dart | 12 ++++-- lib/widgets/viewer/overlay/bottom.dart | 29 ++++++++------ lib/widgets/viewer/overlay/minimap.dart | 2 +- lib/widgets/viewer/overlay/multipage.dart | 5 ++- lib/widgets/viewer/printer.dart | 4 +- .../viewer/visual/entry_page_view.dart | 8 ++-- 24 files changed, 163 insertions(+), 143 deletions(-) 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 82edbd66c..43a8ff81d 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 @@ -58,7 +58,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { val isFlipped = call.argument("isFlipped") val widthDip = call.argument("widthDip") val heightDip = call.argument("heightDip") - val page = call.argument("page") + val pageId = call.argument("pageId") val defaultSizeDip = call.argument("defaultSizeDip") if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) { @@ -76,7 +76,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { isFlipped, width = (widthDip * density).roundToInt(), height = (heightDip * density).roundToInt(), - page = page, + pageId = pageId, defaultSize = (defaultSizeDip * density).roundToInt(), result, ).fetch() @@ -85,7 +85,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { private fun getRegion(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } val mimeType = call.argument("mimeType") - val page = call.argument("page") + val pageId = call.argument("pageId") val sampleSize = call.argument("sampleSize") val x = call.argument("regionX") val y = call.argument("regionY") @@ -105,7 +105,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { uri, sampleSize, regionRect, - page = page ?: 0, + page = pageId ?: 0, result, ) else -> regionFetcher.fetch( diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index e1d693a83..2a5355ba8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -524,20 +524,21 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { return } - val pages = HashMap() + val pages = ArrayList>() if (mimeType == MimeTypes.TIFF) { - fun toMap(options: TiffBitmapFactory.Options): Map { + fun toMap(page: Int, options: TiffBitmapFactory.Options): HashMap { return hashMapOf( + KEY_PAGE to page, KEY_MIME_TYPE to mimeType, KEY_WIDTH to options.outWidth, KEY_HEIGHT to options.outHeight, ) } getTiffPageInfo(uri, 0)?.let { first -> - pages[0] = toMap(first) + pages.add(toMap(0, first)) val pageCount = first.outDirectoryCount for (i in 1 until pageCount) { - getTiffPageInfo(uri, i)?.let { pages[i] = toMap(it) } + getTiffPageInfo(uri, i)?.let { pages.add(toMap(i, it)) } } } } else if (isHeifLike(mimeType)) { @@ -556,14 +557,18 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { val format = extractor.getTrackFormat(i) format.getString(MediaFormat.KEY_MIME)?.let { mime -> val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime - val page = hashMapOf(KEY_MIME_TYPE to trackMime) + val page = hashMapOf( + KEY_PAGE to i, + KEY_MIME_TYPE to trackMime, + ) + format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { page[KEY_IS_DEFAULT] = it != 0 } format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it } format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it } format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it } if (isVideo(trackMime)) { format.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 } } - pages[i] = page + pages.add(page) } } catch (e: Exception) { Log.w(LOG_TAG, "failed to get track information for uri=$uri, track num=$i", e) @@ -778,7 +783,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription" private const val KEY_HEIGHT = "height" private const val KEY_WIDTH = "width" + private const val KEY_PAGE = "page" private const val KEY_TRACK_ID = "trackId" + private const val KEY_IS_DEFAULT = "isDefault" private const val KEY_DURATION = "durationMillis" private const val MASK_IS_ANIMATED = 1 shl 0 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt index d3c4d4605..f5d027d32 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt @@ -34,7 +34,7 @@ class ThumbnailFetcher internal constructor( private val isFlipped: Boolean, width: Int?, height: Int?, - private val page: Int?, + private val pageId: Int?, private val defaultSize: Int, private val result: MethodChannel.Result, ) { @@ -42,7 +42,7 @@ class ThumbnailFetcher internal constructor( private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize private val tiffFetch = mimeType == MimeTypes.TIFF - private val multiTrackFetch = isHeifLike(mimeType) && page != null + private val multiTrackFetch = isHeifLike(mimeType) && pageId != null private val customFetch = tiffFetch || multiTrackFetch fun fetch() { @@ -114,7 +114,7 @@ class ThumbnailFetcher internal constructor( // add signature to ignore cache for images which got modified but kept the same URI var options = RequestOptions() .format(DecodeFormat.PREFER_RGB_565) - .signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$page")) + .signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$pageId")) .override(width, height) val target = if (isVideo(mimeType)) { @@ -125,11 +125,11 @@ class ThumbnailFetcher internal constructor( .load(VideoThumbnail(context, uri)) .submit(width, height) } else { - val model: Any = if (tiffFetch) { - TiffThumbnail(context, uri, page ?: 0) - } else if (multiTrackFetch) { - MultiTrackImage(context, uri, page ?: 0) - } else uri + val model: Any = when { + tiffFetch -> TiffThumbnail(context, uri, pageId ?: 0) + multiTrackFetch -> MultiTrackImage(context, uri, pageId) + else -> uri + } Glide.with(context) .asBitmap() .apply(options) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt index 8a73fa1a2..28022a627 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -86,7 +86,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) } val rotationDegrees = arguments["rotationDegrees"] as Int val isFlipped = arguments["isFlipped"] as Boolean - val page = arguments["page"] as Int? + val pageId = arguments["pageId"] as Int? if (mimeType == null || uri == null) { error("streamImage-args", "failed because of missing arguments", null) @@ -97,10 +97,10 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen if (isVideo(mimeType)) { streamVideoByGlide(uri) } else if (mimeType == MimeTypes.TIFF) { - streamTiffImage(uri, page) + streamTiffImage(uri, pageId) } else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) { // decode exotic format on platform side, then encode it in portable format for Flutter - streamImageByGlide(uri, page, mimeType, rotationDegrees, isFlipped) + streamImageByGlide(uri, pageId, mimeType, rotationDegrees, isFlipped) } else { // to be decoded by Flutter streamImageAsIs(uri) @@ -116,9 +116,9 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } } - private fun streamImageByGlide(uri: Uri, page: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { - val model: Any = if (isHeifLike(mimeType) && page != null) { - MultiTrackImage(activity, uri, page) + private fun streamImageByGlide(uri: Uri, pageId: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { + val model: Any = if (isHeifLike(mimeType) && pageId != null) { + MultiTrackImage(activity, uri, pageId) } else { uri } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt index 36b2d544a..6a53d5709 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt @@ -28,7 +28,7 @@ class MultiTrackImageGlideModule : LibraryGlideModule() { } } -class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int) +class MultiTrackImage(val context: Context, val uri: Uri, val trackId: Int?) internal class MultiTrackThumbnailLoader : ModelLoader { override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData { @@ -53,9 +53,9 @@ internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int val context = model.context val uri = model.uri - val trackIndex = model.trackIndex + val trackId = model.trackId - val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex) + val bitmap = MultiTrackMedia.getImage(context, uri, trackId) if (bitmap == null) { callback.onLoadFailed(Exception("null bitmap")) } else { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt index afc7f4976..8ac8dad05 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiTrackMedia.kt @@ -16,14 +16,17 @@ object MultiTrackMedia { private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java) @RequiresApi(Build.VERSION_CODES.P) - fun getImage(context: Context, uri: Uri, trackIndex: Int): Bitmap? { - val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) ?: return null - + fun getImage(context: Context, uri: Uri, trackId: Int?): Bitmap? { val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null try { - return retriever.getImageAtIndex(imageIndex) + return if (trackId != null) { + val imageIndex = trackIdToImageIndex(context, uri, trackId) ?: return null + retriever.getImageAtIndex(imageIndex) + } else { + retriever.primaryImage + } } catch (e: Exception) { - Log.w(LOG_TAG, "failed to extract image from uri=$uri trackIndex=$trackIndex imageIndex=$imageIndex", e) + Log.w(LOG_TAG, "failed to extract image from uri=$uri trackId=$trackId", e) } finally { // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs retriever.release() @@ -31,21 +34,23 @@ object MultiTrackMedia { return null } - private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? { + private fun trackIdToImageIndex(context: Context, uri: Uri, trackId: Int): Int? { val extractor = MediaExtractor() try { extractor.setDataSource(context, uri, null) val trackCount = extractor.trackCount - if (trackIndex < trackCount) { - var imageIndex = 0 - for (i in 0 until trackIndex) { - val mimeType = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME) - if (MimeTypes.isImage(mimeType)) imageIndex++ + var imageIndex = 0 + for (i in 0 until trackCount) { + val trackFormat = extractor.getTrackFormat(i) + if (trackId == trackFormat.getInteger(MediaFormat.KEY_TRACK_ID)) { + return imageIndex + } + if (MimeTypes.isImage(trackFormat.getString(MediaFormat.KEY_MIME))) { + imageIndex++ } - return imageIndex } } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e) + Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackId=$trackId", e) } finally { extractor.release() } diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index 5d41855c4..3563d9419 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -22,7 +22,7 @@ class RegionProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, region=${key.region}'); + yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, region=${key.region}'); }, ); } @@ -30,7 +30,7 @@ class RegionProvider extends ImageProvider { Future _loadAsync(RegionProviderKey key, DecoderCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; - final page = key.page; + final pageId = key.pageId; try { final bytes = await ImageFileService.getRegion( uri, @@ -40,7 +40,7 @@ class RegionProvider extends ImageProvider { key.sampleSize, key.region, key.imageSize, - page: page, + pageId: pageId, taskKey: key, ); if (bytes == null) { @@ -49,7 +49,7 @@ class RegionProvider extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); - throw StateError('$mimeType region decoding failed (page $page)'); + throw StateError('$mimeType region decoding failed (page $pageId)'); } } @@ -66,7 +66,7 @@ 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 page, rotationDegrees, sampleSize; + final int pageId, rotationDegrees, sampleSize; final bool isFlipped; final Rectangle region; final Size imageSize; @@ -75,7 +75,7 @@ class RegionProviderKey { const RegionProviderKey({ @required this.uri, @required this.mimeType, - @required this.page, + @required this.pageId, @required this.rotationDegrees, @required this.isFlipped, @required this.sampleSize, @@ -94,14 +94,14 @@ class RegionProviderKey { @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale; + return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale; } @override int get hashCode => hashValues( uri, mimeType, - page, + pageId, rotationDegrees, isFlipped, sampleSize, @@ -111,5 +111,5 @@ class RegionProviderKey { ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}'; } diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index 02b4ac2f2..fac117ab4 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -23,7 +23,7 @@ class ThumbnailProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, extent=${key.extent}'); + yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, extent=${key.extent}'); }, ); } @@ -31,12 +31,12 @@ class ThumbnailProvider extends ImageProvider { Future _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; - final page = key.page; + final pageId = key.pageId; try { final bytes = await ImageFileService.getThumbnail( uri: uri, mimeType: mimeType, - page: page, + pageId: pageId, rotationDegrees: key.rotationDegrees, isFlipped: key.isFlipped, dateModifiedSecs: key.dateModifiedSecs, @@ -49,7 +49,7 @@ class ThumbnailProvider extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with uri=$uri, error=$error'); - throw StateError('$mimeType decoding failed (page $page)'); + throw StateError('$mimeType decoding failed (page $pageId)'); } } @@ -66,7 +66,7 @@ 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 page, rotationDegrees; + final int pageId, rotationDegrees; final bool isFlipped; final int dateModifiedSecs; final double extent, scale; @@ -74,7 +74,7 @@ class ThumbnailProviderKey { const ThumbnailProviderKey({ @required this.uri, @required this.mimeType, - @required this.page, + @required this.pageId, @required this.rotationDegrees, @required this.isFlipped, @required this.dateModifiedSecs, @@ -91,14 +91,14 @@ class ThumbnailProviderKey { @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale; + return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale; } @override int get hashCode => hashValues( uri, mimeType, - page, + pageId, rotationDegrees, isFlipped, dateModifiedSecs, @@ -107,5 +107,5 @@ class ThumbnailProviderKey { ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}'; } diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index 4e2d3f46c..6c3f9615e 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -8,14 +8,14 @@ import 'package:pedantic/pedantic.dart'; class UriImage extends ImageProvider { final String uri, mimeType; - final int page, rotationDegrees, expectedContentLength; + final int pageId, rotationDegrees, expectedContentLength; final bool isFlipped; final double scale; const UriImage({ @required this.uri, @required this.mimeType, - @required this.page, + @required this.pageId, @required this.rotationDegrees, @required this.isFlipped, this.expectedContentLength, @@ -37,7 +37,7 @@ class UriImage extends ImageProvider { scale: key.scale, chunkEvents: chunkEvents.stream, informationCollector: () sync* { - yield ErrorDescription('uri=$uri, page=$page, mimeType=$mimeType'); + yield ErrorDescription('uri=$uri, pageId=$pageId, mimeType=$mimeType'); }, ); } @@ -51,7 +51,7 @@ class UriImage extends ImageProvider { mimeType, rotationDegrees, isFlipped, - page: page, + pageId: pageId, expectedContentLength: expectedContentLength, onBytesReceived: (cumulative, total) { chunkEvents.add(ImageChunkEvent( @@ -66,7 +66,7 @@ class UriImage extends ImageProvider { return await decode(bytes); } catch (error) { debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); - throw StateError('$mimeType decoding failed (page $page)'); + throw StateError('$mimeType decoding failed (page $pageId)'); } finally { unawaited(chunkEvents.close()); } @@ -75,7 +75,7 @@ class UriImage extends ImageProvider { @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale; + return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.pageId == pageId && other.scale == scale; } @override @@ -84,10 +84,10 @@ class UriImage extends ImageProvider { mimeType, rotationDegrees, isFlipped, - page, + pageId, scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, pageId=$pageId, scale=$scale}'; } diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 6d3588236..b15b634ba 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -24,7 +24,7 @@ import '../ref/mime_types.dart'; class AvesEntry { String uri; String _path, _directory, _filename, _extension; - int page, contentId; + int pageId, contentId; final String sourceMimeType; int width; int height; @@ -49,7 +49,7 @@ class AvesEntry { this.uri, String path, this.contentId, - this.page, + this.pageId, this.sourceMimeType, @required this.width, @required this.height, @@ -96,18 +96,14 @@ class AvesEntry { return copied; } - AvesEntry getPageEntry({ - @required MultiPageInfo multiPageInfo, - @required int page, - }) { - final pageInfo = (multiPageInfo?.pages ?? {})[page]; + AvesEntry getPageEntry(SinglePageInfo pageInfo) { if (pageInfo == null) return this; return AvesPageEntry( pageInfo: pageInfo, uri: uri, path: path, contentId: contentId, - page: page, + pageId: pageInfo.pageId, sourceMimeType: sourceMimeType, width: width, height: height, @@ -168,7 +164,7 @@ class AvesEntry { } @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}'; set path(String path) { _path = path; @@ -227,7 +223,7 @@ class AvesEntry { MimeTypes.srw, ].contains(mimeType) && !isAnimated && - page == null; + pageId == null; bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; diff --git a/lib/model/entry_cache.dart b/lib/model/entry_cache.dart index 291cac72f..1e794f202 100644 --- a/lib/model/entry_cache.dart +++ b/lib/model/entry_cache.dart @@ -12,14 +12,14 @@ class EntryCache { int oldRotationDegrees, bool oldIsFlipped, ) async { - // TODO TLAD provide page parameter for multipage items, if someday image editing features are added for them - int page; + // TODO TLAD provide pageId parameter for multipage items, if someday image editing features are added for them + int pageId; // evict fullscreen image await UriImage( uri: uri, mimeType: mimeType, - page: page, + pageId: pageId, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, ).evict(); @@ -28,7 +28,7 @@ class EntryCache { await ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, - page: page, + pageId: pageId, dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, @@ -41,7 +41,7 @@ class EntryCache { (extent) => ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, - page: page, + pageId: pageId, dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart index 85bd93828..72ab8493e 100644 --- a/lib/model/entry_images.dart +++ b/lib/model/entry_images.dart @@ -20,7 +20,7 @@ extension ExtraAvesEntry on AvesEntry { return ThumbnailProviderKey( uri: uri, mimeType: mimeType, - page: page, + pageId: pageId, rotationDegrees: rotationDegrees, isFlipped: isFlipped, dateModifiedSecs: dateModifiedSecs ?? -1, @@ -34,7 +34,7 @@ extension ExtraAvesEntry on AvesEntry { return RegionProviderKey( uri: uri, mimeType: mimeType, - page: page, + pageId: pageId, rotationDegrees: rotationDegrees, isFlipped: isFlipped, sampleSize: sampleSize, @@ -46,7 +46,7 @@ extension ExtraAvesEntry on AvesEntry { UriImage get uriImage => UriImage( uri: uri, mimeType: mimeType, - page: page, + pageId: pageId, rotationDegrees: rotationDegrees, isFlipped: isFlipped, expectedContentLength: sizeBytes, diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index e61bc0f8a..a9f4d6222 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -2,7 +2,7 @@ import 'package:aves/model/entry.dart'; import 'package:flutter/foundation.dart'; class MultiPageInfo { - final Map pages; + final List pages; int get pageCount => pages.length; @@ -10,44 +10,49 @@ class MultiPageInfo { this.pages, }); - factory MultiPageInfo.fromMap(Map map) { - final pages = {}; - map.keys.forEach((key) { - final index = key as int; - pages.putIfAbsent(index, () => SinglePageInfo.fromMap(map[key])); - }); - return MultiPageInfo(pages: pages); + factory MultiPageInfo.fromPageMaps(List pageMaps) { + return MultiPageInfo(pages: pageMaps.map((page) => SinglePageInfo.fromMap(page)).toList()); } + SinglePageInfo getByIndex(int index) => pages.firstWhere((page) => page.index == index, orElse: () => null); + + SinglePageInfo getById(int pageId) => pages.firstWhere((page) => page.pageId == pageId, orElse: () => null); + @override String toString() => '$runtimeType#${shortHash(this)}{pages=$pages}'; } class SinglePageInfo { + final int index, pageId; final String mimeType; - final int width, height; - final int trackId, durationMillis; + final bool isDefault; + final int width, height, durationMillis; SinglePageInfo({ + this.index, + this.pageId, this.mimeType, + this.isDefault, this.width, this.height, - this.trackId, this.durationMillis, }); factory SinglePageInfo.fromMap(Map map) { + final index = map['page'] as int; return SinglePageInfo( + index: index, + pageId: map['trackId'] as int ?? index, mimeType: map['mimeType'] as String, - width: map['width'] as int, - height: map['height'] as int, - trackId: map['trackId'] as int, + isDefault: map['isDefault'] as bool ?? false, + width: map['width'] as int ?? 0, + height: map['height'] as int ?? 0, durationMillis: map['durationMillis'] as int, ); } @override - String toString() => '$runtimeType#${shortHash(this)}{mimeType=$mimeType, width=$width, height=$height, trackId=$trackId, durationMillis=$durationMillis}'; + String toString() => '$runtimeType#${shortHash(this)}{index=$index, pageId=$pageId, mimeType=$mimeType, isDefault=$isDefault, width=$width, height=$height, durationMillis=$durationMillis}'; } class AvesPageEntry extends AvesEntry { @@ -58,7 +63,7 @@ class AvesPageEntry extends AvesEntry { String uri, String path, int contentId, - int page, + int pageId, String sourceMimeType, int width, int height, @@ -72,7 +77,7 @@ class AvesPageEntry extends AvesEntry { uri: uri, path: path, contentId: contentId, - page: page, + pageId: pageId, sourceMimeType: pageInfo.mimeType ?? sourceMimeType, width: pageInfo.width ?? width, height: pageInfo.height ?? height, diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart index a358b5413..02a3f76f6 100644 --- a/lib/services/app_shortcut_service.dart +++ b/lib/services/app_shortcut_service.dart @@ -33,7 +33,7 @@ class AppShortcutService { iconBytes = await ImageFileService.getThumbnail( uri: entry.uri, mimeType: entry.mimeType, - page: entry.page, + pageId: entry.pageId, rotationDegrees: entry.rotationDegrees, isFlipped: entry.isFlipped, dateModifiedSecs: entry.dateModifiedSecs, diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index af68d8bd9..8e94d0d59 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -89,7 +89,7 @@ class ImageFileService { String mimeType, int rotationDegrees, bool isFlipped, { - int page, + int pageId, int expectedContentLength, BytesReceivedCallback onBytesReceived, }) { @@ -102,7 +102,7 @@ class ImageFileService { 'mimeType': mimeType, 'rotationDegrees': rotationDegrees ?? 0, 'isFlipped': isFlipped ?? false, - 'page': page, + 'pageId': pageId, }).listen( (data) { final chunk = data as Uint8List; @@ -140,7 +140,7 @@ class ImageFileService { int sampleSize, Rectangle regionRect, Size imageSize, { - int page, + int pageId, Object taskKey, int priority, }) { @@ -150,7 +150,7 @@ class ImageFileService { final result = await platform.invokeMethod('getRegion', { 'uri': uri, 'mimeType': mimeType, - 'page': page, + 'pageId': pageId, 'sampleSize': sampleSize, 'regionX': regionRect.left, 'regionY': regionRect.top, @@ -174,7 +174,7 @@ class ImageFileService { @required String uri, @required String mimeType, @required int rotationDegrees, - @required int page, + @required int pageId, @required bool isFlipped, @required int dateModifiedSecs, @required double extent, @@ -195,7 +195,7 @@ class ImageFileService { 'isFlipped': isFlipped, 'widthDip': extent, 'heightDip': extent, - 'page': page, + 'pageId': pageId, 'defaultSizeDip': thumbnailDefaultSize, }); return result as Uint8List; diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index bb058efda..e4a2e0b90 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -87,8 +87,9 @@ class MetadataService { final result = await platform.invokeMethod('getMultiPageInfo', { 'mimeType': entry.mimeType, 'uri': entry.uri, - }) as Map; - return MultiPageInfo.fromMap(result); + }); + final pageMaps = (result as List).cast(); + return MultiPageInfo.fromPageMaps(pageMaps); } on PlatformException catch (e) { debugPrint('getMultiPageInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index dcfb8a786..5a37c8367 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -9,7 +9,6 @@ import 'package:flutter/material.dart'; class RasterImageThumbnail extends StatefulWidget { final AvesEntry entry; final double extent; - final int page; final ValueNotifier isScrollingNotifier; final Object heroTag; @@ -17,7 +16,6 @@ class RasterImageThumbnail extends StatefulWidget { Key key, @required this.entry, @required this.extent, - this.page, this.isScrollingNotifier, this.heroTag, }) : super(key: key); diff --git a/lib/widgets/viewer/entry_scroller.dart b/lib/widgets/viewer/entry_scroller.dart index d4ec97c0e..7927597a4 100644 --- a/lib/widgets/viewer/entry_scroller.dart +++ b/lib/widgets/viewer/entry_scroller.dart @@ -62,7 +62,7 @@ class _MultiEntryScrollerState extends State with AutomaticK return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { - return _buildViewer(entry, multiPageInfo: multiPageInfo, page: page); + return _buildViewer(entry, page: multiPageInfo?.getByIndex(page)); }, ); }, @@ -80,14 +80,13 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - Widget _buildViewer(AvesEntry entry, {MultiPageInfo multiPageInfo, int page}) { + Widget _buildViewer(AvesEntry entry, {SinglePageInfo page}) { return Selector( selector: (c, mq) => mq.size, builder: (c, mqSize, child) { return EntryPageView( key: Key('imageview'), mainEntry: entry, - multiPageInfo: multiPageInfo, page: page, viewportSize: mqSize, heroTag: widget.collection.heroTag(entry), @@ -142,7 +141,7 @@ class _SingleEntryScrollerState extends State with Automati return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { - return _buildViewer(multiPageInfo: multiPageInfo, page: page); + return _buildViewer(page: multiPageInfo?.getByIndex(page)); }, ); }, @@ -157,13 +156,12 @@ class _SingleEntryScrollerState extends State with Automati ); } - Widget _buildViewer({MultiPageInfo multiPageInfo, int page}) { + Widget _buildViewer({SinglePageInfo page}) { return Selector( selector: (c, mq) => mq.size, builder: (c, mqSize, child) { return EntryPageView( mainEntry: entry, - multiPageInfo: multiPageInfo, page: page, viewportSize: mqSize, onTap: (_) => widget.onTap?.call(), diff --git a/lib/widgets/viewer/multipage.dart b/lib/widgets/viewer/multipage.dart index 7bc74b9ac..9fae90a5b 100644 --- a/lib/widgets/viewer/multipage.dart +++ b/lib/widgets/viewer/multipage.dart @@ -7,10 +7,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class MultiPageController extends ChangeNotifier { - final Future info; - final ValueNotifier pageNotifier = ValueNotifier(0); + Future info; + final ValueNotifier pageNotifier = ValueNotifier(null); - MultiPageController(AvesEntry entry) : info = MetadataService.getMultiPageInfo(entry); + MultiPageController(AvesEntry entry) { + info = MetadataService.getMultiPageInfo(entry).then((value) { + final defaultPage = value.pages.firstWhere((page) => page.isDefault, orElse: () => null); + pageNotifier.value = defaultPage?.index ?? 0; + return value; + }); + } int get page => pageNotifier.value; diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 24e7bd2fe..c1a498a25 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -101,8 +101,7 @@ class _ViewerBottomOverlayState extends State { Widget _buildContent({MultiPageInfo multiPageInfo, int page}) => _BottomOverlayContent( mainEntry: _lastEntry, - multiPageInfo: multiPageInfo, - page: page, + page: multiPageInfo?.getByIndex(page), details: _lastDetails, position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null, availableWidth: availableWidth, @@ -139,8 +138,7 @@ const double _subRowMinWidth = 300.0; class _BottomOverlayContent extends AnimatedWidget { final AvesEntry mainEntry, entry; - final MultiPageInfo multiPageInfo; - final int page; + final SinglePageInfo page; final OverlayMetadata details; final String position; final double availableWidth; @@ -151,13 +149,12 @@ class _BottomOverlayContent extends AnimatedWidget { _BottomOverlayContent({ Key key, this.mainEntry, - this.multiPageInfo, this.page, this.details, this.position, this.availableWidth, this.multiPageController, - }) : entry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page), + }) : entry = mainEntry.getPageEntry(page), super(key: key, listenable: mainEntry.metadataChangeNotifier); @override @@ -342,6 +339,8 @@ class _PositionTitleRow extends StatelessWidget { bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null; + static const separator = ' • '; + @override Widget build(BuildContext context) { Text toText({String pagePosition}) => Text( @@ -349,7 +348,7 @@ class _PositionTitleRow extends StatelessWidget { if (collectionPosition != null) collectionPosition, if (pagePosition != null) pagePosition, if (title != null) title, - ].join(' • '), + ].join(separator), strutStyle: Constants.overflowStrutStyle); if (multiPageController == null) return toText(); @@ -358,11 +357,17 @@ class _PositionTitleRow extends StatelessWidget { future: multiPageController.info, builder: (context, snapshot) { final multiPageInfo = snapshot.data; - final pageCount = multiPageInfo?.pageCount; - // page count may be 0 when we know an entry to have multiple pages - // but fail to get information about these pages - final missingInfo = pageCount == 0; - return toText(pagePosition: missingInfo ? null : '${(entry.page ?? 0) + 1}/${pageCount ?? '?'}'); + 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 + final pageCount = multiPageInfo.pageCount; + if (pageCount > 0) { + final page = multiPageInfo.getById(entry.pageId); + pagePosition = '${(page?.index ?? 0) + 1}/$pageCount'; + } + } + return toText(pagePosition: pagePosition); }, ); } diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index 657aff590..794de067a 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -34,7 +34,7 @@ class Minimap extends StatelessWidget { return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { - final pageEntry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page); + final pageEntry = mainEntry.getPageEntry(multiPageInfo?.getByIndex(page)); return _buildForEntrySize(pageEntry.displaySize); }, ); diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index b379a350e..b1568b837 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -62,7 +62,8 @@ class _MultiPageOverlayState extends State { } void _registerWidget() { - final scrollOffset = pageToScrollOffset(controller.page); + final page = controller.page ?? 0; + final scrollOffset = pageToScrollOffset(page); _scrollController = ScrollController(initialScrollOffset: scrollOffset); _scrollController.addListener(_onScrollChange); } @@ -108,7 +109,7 @@ class _MultiPageOverlayState extends State { itemBuilder: (context, index) { if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin; final page = index - 1; - final pageEntry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page); + final pageEntry = mainEntry.getPageEntry(multiPageInfo.getByIndex(page)); return GestureDetector( onTap: () async { diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index d3ab1a35c..bab2dc5a7 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -46,8 +46,8 @@ class EntryPrinter { if (entry.isMultipage) { final multiPageInfo = await MetadataService.getMultiPageInfo(entry); if (multiPageInfo.pageCount > 1) { - for (final kv in multiPageInfo.pages.entries) { - final pageEntry = entry.getPageEntry(multiPageInfo: multiPageInfo, page: kv.key); + for (final page in multiPageInfo.pages) { + final pageEntry = entry.getPageEntry(page); _addPdfPage(await _buildPageImage(pageEntry)); } } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index fcc8f4e70..30b4f3830 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -24,8 +24,7 @@ import 'package:tuple/tuple.dart'; class EntryPageView extends StatefulWidget { final AvesEntry entry; - final MultiPageInfo multiPageInfo; - final int page; + final SinglePageInfo page; final Size viewportSize; final Object heroTag; final MagnifierTapCallback onTap; @@ -37,14 +36,13 @@ class EntryPageView extends StatefulWidget { EntryPageView({ Key key, AvesEntry mainEntry, - this.multiPageInfo, this.page, this.viewportSize, this.heroTag, @required this.onTap, @required this.videoControllers, this.onDisposed, - }) : entry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page) ?? mainEntry, + }) : entry = mainEntry.getPageEntry(page) ?? mainEntry, super(key: key); @override @@ -198,7 +196,7 @@ class _EntryPageViewState extends State { }) { return Magnifier( // key includes size and orientation to refresh when the image is rotated - key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), + key: ValueKey('${entry.pageId}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), controller: _magnifierController, childSize: entry.displaySize, minScale: minScale, From b0cccd7d2d5d0f7bd62ac505f9479d05417de5d6 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 22 Jan 2021 14:52:19 +0900 Subject: [PATCH 20/44] multipage: faster default page --- lib/model/entry.dart | 19 ++++--- lib/model/entry_images.dart | 10 ++-- lib/model/multipage.dart | 68 ++++++++++++-------------- lib/widgets/viewer/multipage.dart | 3 +- lib/widgets/viewer/overlay/bottom.dart | 2 +- 5 files changed, 51 insertions(+), 51 deletions(-) diff --git a/lib/model/entry.dart b/lib/model/entry.dart index b15b634ba..668d64ddc 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -98,21 +98,26 @@ class AvesEntry { AvesEntry getPageEntry(SinglePageInfo pageInfo) { if (pageInfo == null) return this; - return AvesPageEntry( - pageInfo: pageInfo, + + // do not provide the page ID for the default page, + // so that we can treat this page like the main entry + // and retrieve cached images for it + final pageId = pageInfo.isDefault ? null : pageInfo.pageId; + + return AvesEntry( uri: uri, path: path, contentId: contentId, - pageId: pageInfo.pageId, - sourceMimeType: sourceMimeType, - width: width, - height: height, + pageId: pageId, + sourceMimeType: pageInfo.mimeType ?? sourceMimeType, + width: pageInfo.width ?? width, + height: pageInfo.height ?? height, sourceRotationDegrees: sourceRotationDegrees, sizeBytes: sizeBytes, sourceTitle: sourceTitle, dateModifiedSecs: dateModifiedSecs, sourceDateTakenMillis: sourceDateTakenMillis, - durationMillis: durationMillis, + durationMillis: pageInfo.durationMillis ?? durationMillis, ) ..catalogMetadata = _catalogMetadata?.copyWith( mimeType: pageInfo.mimeType, diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart index 72ab8493e..cc8c28374 100644 --- a/lib/model/entry_images.dart +++ b/lib/model/entry_images.dart @@ -9,7 +9,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; extension ExtraAvesEntry on AvesEntry { - ThumbnailProvider getThumbnail({double extent = 0}) => ThumbnailProvider(_getThumbnailProviderKey(extent)); + ThumbnailProvider getThumbnail({double extent = 0}) { + return ThumbnailProvider(_getThumbnailProviderKey(extent)); + } ThumbnailProviderKey _getThumbnailProviderKey(double extent) { // we standardize the thumbnail loading dimension by taking the nearest larger power of 2 @@ -28,9 +30,11 @@ extension ExtraAvesEntry on AvesEntry { ); } - RegionProvider getRegion({@required int sampleSize, Rectangle region}) => RegionProvider(getRegionProviderKey(sampleSize, 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, diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index a9f4d6222..9400c1beb 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/entry.dart'; import 'package:flutter/foundation.dart'; class MultiPageInfo { @@ -8,12 +7,23 @@ class MultiPageInfo { MultiPageInfo({ this.pages, - }); + }) { + if (pages.isNotEmpty) { + pages.sort(); + // make sure there is a page marked as default + if (defaultPage == null) { + final firstPage = pages.removeAt(0); + pages.insert(0, firstPage.copyWith(isDefault: true)); + } + } + } factory MultiPageInfo.fromPageMaps(List pageMaps) { return MultiPageInfo(pages: pageMaps.map((page) => SinglePageInfo.fromMap(page)).toList()); } + SinglePageInfo get defaultPage => pages.firstWhere((page) => page.isDefault, orElse: () => null); + SinglePageInfo getByIndex(int index) => pages.firstWhere((page) => page.index == index, orElse: () => null); SinglePageInfo getById(int pageId) => pages.firstWhere((page) => page.pageId == pageId, orElse: () => null); @@ -22,13 +32,13 @@ class MultiPageInfo { String toString() => '$runtimeType#${shortHash(this)}{pages=$pages}'; } -class SinglePageInfo { +class SinglePageInfo implements Comparable { final int index, pageId; final String mimeType; final bool isDefault; final int width, height, durationMillis; - SinglePageInfo({ + const SinglePageInfo({ this.index, this.pageId, this.mimeType, @@ -38,6 +48,20 @@ class SinglePageInfo { this.durationMillis, }); + SinglePageInfo copyWith({ + bool isDefault, + }) { + return SinglePageInfo( + index: index, + pageId: pageId, + mimeType: mimeType, + isDefault: isDefault ?? this.isDefault, + width: width, + height: height, + durationMillis: durationMillis, + ); + } + factory SinglePageInfo.fromMap(Map map) { final index = map['page'] as int; return SinglePageInfo( @@ -53,39 +77,7 @@ class SinglePageInfo { @override String toString() => '$runtimeType#${shortHash(this)}{index=$index, pageId=$pageId, mimeType=$mimeType, isDefault=$isDefault, width=$width, height=$height, durationMillis=$durationMillis}'; -} -class AvesPageEntry extends AvesEntry { - final SinglePageInfo pageInfo; - - AvesPageEntry({ - @required this.pageInfo, - String uri, - String path, - int contentId, - int pageId, - String sourceMimeType, - int width, - int height, - int sourceRotationDegrees, - int sizeBytes, - String sourceTitle, - int dateModifiedSecs, - int sourceDateTakenMillis, - int durationMillis, - }) : super( - uri: uri, - path: path, - contentId: contentId, - pageId: pageId, - sourceMimeType: pageInfo.mimeType ?? sourceMimeType, - width: pageInfo.width ?? width, - height: pageInfo.height ?? height, - sourceRotationDegrees: sourceRotationDegrees, - sizeBytes: sizeBytes, - sourceTitle: sourceTitle, - dateModifiedSecs: dateModifiedSecs, - sourceDateTakenMillis: sourceDateTakenMillis, - durationMillis: pageInfo.durationMillis ?? durationMillis, - ); + @override + int compareTo(SinglePageInfo other) => index.compareTo(other.index); } diff --git a/lib/widgets/viewer/multipage.dart b/lib/widgets/viewer/multipage.dart index 9fae90a5b..91de82859 100644 --- a/lib/widgets/viewer/multipage.dart +++ b/lib/widgets/viewer/multipage.dart @@ -12,8 +12,7 @@ class MultiPageController extends ChangeNotifier { MultiPageController(AvesEntry entry) { info = MetadataService.getMultiPageInfo(entry).then((value) { - final defaultPage = value.pages.firstWhere((page) => page.isDefault, orElse: () => null); - pageNotifier.value = defaultPage?.index ?? 0; + pageNotifier.value = value.defaultPage.index; return value; }); } diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index c1a498a25..2696d5199 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -363,7 +363,7 @@ class _PositionTitleRow extends StatelessWidget { // but fail to get information about these pages final pageCount = multiPageInfo.pageCount; if (pageCount > 0) { - final page = multiPageInfo.getById(entry.pageId); + final page = multiPageInfo.getById(entry.pageId) ?? multiPageInfo.defaultPage; pagePosition = '${(page?.index ?? 0) + 1}/$pageCount'; } } From c4fdd38850e931bca8d00974a423894855fc775b Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 24 Jan 2021 14:15:46 +0900 Subject: [PATCH 21/44] export: to jpeg, no metadata --- .../aves/channel/calls/ThumbnailFetcher.kt | 4 +- .../channel/streams/ImageByteStreamHandler.kt | 28 +--- .../channel/streams/ImageOpStreamHandler.kt | 91 +++++++---- .../decoder/MultiTrackImageGlideModule.kt | 22 ++- .../thibault/aves/decoder/TiffGlideModule.kt | 99 ++++++++++++ .../aves/decoder/TiffThumbnailGlideModule.kt | 99 ------------ .../deckers/thibault/aves/model/AvesEntry.kt | 2 + .../aves/model/provider/ImageProvider.kt | 4 + .../model/provider/MediaStoreImageProvider.kt | 147 ++++++++++++++++++ .../thibault/aves/utils/BitmapUtils.kt | 2 +- .../deckers/thibault/aves/utils/MimeTypes.kt | 4 +- lib/model/actions/entry_actions.dart | 6 + lib/model/actions/move_type.dart | 1 + lib/model/entry.dart | 10 +- lib/model/source/collection_source.dart | 4 +- lib/services/image_file_service.dart | 80 ++++------ lib/services/image_op_events.dart | 85 ++++++++++ lib/theme/icons.dart | 3 +- .../collection/entry_set_action_delegate.dart | 13 +- .../common/action_mixins/feedback.dart | 2 +- .../common/action_mixins/size_aware.dart | 32 ++-- lib/widgets/common/basic/link_chip.dart | 2 +- lib/widgets/filter_grids/album_pick.dart | 26 +++- .../common/chip_action_delegate.dart | 4 +- lib/widgets/viewer/debug_page.dart | 1 - lib/widgets/viewer/entry_action_delegate.dart | 67 +++++++- lib/widgets/viewer/info/maps/common.dart | 2 +- lib/widgets/viewer/overlay/top.dart | 23 +-- lib/widgets/viewer/overlay/video.dart | 2 +- 29 files changed, 592 insertions(+), 273 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt delete mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt create mode 100644 lib/model/actions/move_type.dart create mode 100644 lib/services/image_op_events.dart diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt index f5d027d32..5d9f83388 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt @@ -14,7 +14,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.signature.ObjectKey import deckers.thibault.aves.decoder.MultiTrackImage -import deckers.thibault.aves.decoder.TiffThumbnail +import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.getBytes @@ -126,7 +126,7 @@ class ThumbnailFetcher internal constructor( .submit(width, height) } else { val model: Any = when { - tiffFetch -> TiffThumbnail(context, uri, pageId ?: 0) + tiffFetch -> TiffImage(context, uri, pageId) multiTrackFetch -> MultiTrackImage(context, uri, pageId) else -> uri } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt index 28022a627..b0e464483 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -10,6 +10,7 @@ import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import deckers.thibault.aves.decoder.MultiTrackImage +import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.getBytes @@ -25,7 +26,6 @@ import io.flutter.plugin.common.EventChannel.EventSink import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.IOException import java.io.InputStream @@ -96,8 +96,6 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen if (isVideo(mimeType)) { streamVideoByGlide(uri) - } else if (mimeType == MimeTypes.TIFF) { - streamTiffImage(uri, pageId) } else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) { // decode exotic format on platform side, then encode it in portable format for Flutter streamImageByGlide(uri, pageId, mimeType, rotationDegrees, isFlipped) @@ -119,6 +117,8 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen private fun streamImageByGlide(uri: Uri, pageId: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { val model: Any = if (isHeifLike(mimeType) && pageId != null) { MultiTrackImage(activity, uri, pageId) + } else if (mimeType == MimeTypes.TIFF) { + TiffImage(activity, uri, pageId) } else { uri } @@ -165,28 +165,6 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } } - private fun streamTiffImage(uri: Uri, page: Int?) { - val resolver = activity.contentResolver - try { - val fd = resolver.openFileDescriptor(uri, "r")?.detachFd() - if (fd == null) { - error("streamImage-tiff-fd", "failed to get file descriptor for uri=$uri", null) - return - } - val options = TiffBitmapFactory.Options().apply { - inDirectoryNumber = page ?: 0 - } - val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) - if (bitmap != null) { - success(bitmap.getBytes(canHaveAlpha = true, recycle = true)) - } else { - error("streamImage-tiff-null", "failed to get tiff image (dir=$page) from uri=$uri", null) - } - } catch (e: Exception) { - error("streamImage-tiff-exception", "failed to get image from uri=$uri", toErrorDetails(e)) - } - } - private fun toErrorDetails(e: Exception): String? { val errorDetails = e.message return if (errorDetails?.isNotEmpty() == true) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index de45c8817..b70de372a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -42,6 +42,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments: when (op) { "delete" -> GlobalScope.launch(Dispatchers.IO) { delete() } + "export" -> GlobalScope.launch(Dispatchers.IO) { export() } "move" -> GlobalScope.launch(Dispatchers.IO) { move() } else -> endOfStream() } @@ -80,36 +81,6 @@ class ImageOpStreamHandler(private val context: Context, private val arguments: } } - private suspend fun move() { - if (arguments !is Map<*, *> || entryMapList.isEmpty()) { - endOfStream() - return - } - - // assume same provider for all entries - val firstEntry = entryMapList.first() - val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } - if (provider == null) { - error("move-provider", "failed to find provider for entry=$firstEntry", null) - return - } - - val copy = arguments["copy"] as Boolean? - var destinationDir = arguments["destinationPath"] as String? - if (copy == null || destinationDir == null) { - error("move-args", "failed because of missing arguments", null) - return - } - - destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) - val entries = entryMapList.map(::AvesEntry) - provider.moveMultiple(context, copy, destinationDir, entries, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) = success(fields) - override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable) - }) - endOfStream() - } - private suspend fun delete() { if (entryMapList.isEmpty()) { endOfStream() @@ -144,6 +115,66 @@ class ImageOpStreamHandler(private val context: Context, private val arguments: endOfStream() } + private suspend fun export() { + if (arguments !is Map<*, *> || entryMapList.isEmpty()) { + endOfStream() + return + } + + var destinationDir = arguments["destinationPath"] as String? + val mimeType = arguments["mimeType"] as String? + if (destinationDir == null || mimeType == null) { + error("export-args", "failed because of missing arguments", null) + return + } + + // assume same provider for all entries + val firstEntry = entryMapList.first() + val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } + if (provider == null) { + error("export-provider", "failed to find provider for entry=$firstEntry", null) + return + } + + destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) + val entries = entryMapList.map(::AvesEntry) + provider.exportMultiple(context, mimeType, destinationDir, entries, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = success(fields) + override fun onFailure(throwable: Throwable) = error("export-failure", "failed to export entries", throwable) + }) + endOfStream() + } + + private suspend fun move() { + if (arguments !is Map<*, *> || entryMapList.isEmpty()) { + endOfStream() + return + } + + val copy = arguments["copy"] as Boolean? + var destinationDir = arguments["destinationPath"] as String? + if (copy == null || destinationDir == null) { + error("move-args", "failed because of missing arguments", null) + return + } + + // assume same provider for all entries + val firstEntry = entryMapList.first() + val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } + if (provider == null) { + error("move-provider", "failed to find provider for entry=$firstEntry", null) + return + } + + destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) + val entries = entryMapList.map(::AvesEntry) + provider.moveMultiple(context, copy, destinationDir, entries, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = success(fields) + override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable) + }) + endOfStream() + } + companion object { private val LOG_TAG = LogUtils.createTag(ImageOpStreamHandler::class.java) const val CHANNEL = "deckers.thibault/aves/imageopstream" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt index 6a53d5709..290b7badd 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/MultiTrackImageGlideModule.kt @@ -1,6 +1,7 @@ package deckers.thibault.aves.decoder import android.content.Context +import android.graphics.Bitmap import android.net.Uri import android.os.Build import com.bumptech.glide.Glide @@ -17,35 +18,33 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.module.LibraryGlideModule import com.bumptech.glide.signature.ObjectKey import deckers.thibault.aves.metadata.MultiTrackMedia -import deckers.thibault.aves.utils.BitmapUtils.getBytes -import java.io.InputStream @GlideModule class MultiTrackImageGlideModule : LibraryGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - registry.append(MultiTrackImage::class.java, InputStream::class.java, MultiTrackThumbnailLoader.Factory()) + registry.append(MultiTrackImage::class.java, Bitmap::class.java, MultiTrackThumbnailLoader.Factory()) } } class MultiTrackImage(val context: Context, val uri: Uri, val trackId: Int?) -internal class MultiTrackThumbnailLoader : ModelLoader { - override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData { +internal class MultiTrackThumbnailLoader : ModelLoader { + override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData { return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackImageFetcher(model, width, height)) } override fun handles(model: MultiTrackImage): Boolean = true - internal class Factory : ModelLoaderFactory { - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = MultiTrackThumbnailLoader() + internal class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = MultiTrackThumbnailLoader() override fun teardown() {} } } -internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int, val height: Int) : DataFetcher { - override fun loadData(priority: Priority, callback: DataCallback) { +internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int, val height: Int) : DataFetcher { + override fun loadData(priority: Priority, callback: DataCallback) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { callback.onLoadFailed(Exception("unsupported Android version")) return @@ -59,17 +58,16 @@ internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int if (bitmap == null) { callback.onLoadFailed(Exception("null bitmap")) } else { - callback.onDataReady(bitmap.getBytes()?.inputStream()) + callback.onDataReady(bitmap) } } - // already cleaned up in loadData and ByteArrayInputStream will be GC'd override fun cleanup() {} // cannot cancel override fun cancel() {} - override fun getDataClass(): Class = InputStream::class.java + override fun getDataClass(): Class = Bitmap::class.java override fun getDataSource(): DataSource = DataSource.LOCAL } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt new file mode 100644 index 000000000..074f06332 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt @@ -0,0 +1,99 @@ +package deckers.thibault.aves.decoder + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import com.bumptech.glide.Glide +import com.bumptech.glide.Priority +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.data.DataFetcher.DataCallback +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.module.LibraryGlideModule +import com.bumptech.glide.signature.ObjectKey +import org.beyka.tiffbitmapfactory.TiffBitmapFactory + +@GlideModule +class TiffGlideModule : LibraryGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.append(TiffImage::class.java, Bitmap::class.java, TiffLoader.Factory()) + } +} + +class TiffImage(val context: Context, val uri: Uri, val page: Int?) + +internal class TiffLoader : ModelLoader { + override fun buildLoadData(model: TiffImage, width: Int, height: Int, options: Options): ModelLoader.LoadData { + return ModelLoader.LoadData(ObjectKey(model.uri), TiffFetcher(model, width, height)) + } + + override fun handles(model: TiffImage): Boolean = true + + internal class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = TiffLoader() + + override fun teardown() {} + } +} + +internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int) : DataFetcher { + override fun loadData(priority: Priority, callback: DataCallback) { + val context = model.context + val uri = model.uri + val page = model.page ?: 0 + + var sampleSize = 1 + if (width > 0 && height > 0) { + // determine sample size + val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() + if (fd == null) { + callback.onLoadFailed(Exception("null file descriptor")) + return + } + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = true + inDirectoryNumber = page + } + TiffBitmapFactory.decodeFileDescriptor(fd, options) + val imageWidth = options.outWidth + val imageHeight = options.outHeight + if (imageHeight > height || imageWidth > width) { + while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) { + sampleSize *= 2 + } + } + } + + // decode + val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() + if (fd == null) { + callback.onLoadFailed(Exception("null file descriptor")) + return + } + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = false + inDirectoryNumber = page + inSampleSize = sampleSize + } + val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) + if (bitmap == null) { + callback.onLoadFailed(Exception("null bitmap")) + } else { + callback.onDataReady(bitmap) + } + } + + override fun cleanup() {} + + // cannot cancel + override fun cancel() {} + + override fun getDataClass(): Class = Bitmap::class.java + + override fun getDataSource(): DataSource = DataSource.LOCAL +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt deleted file mode 100644 index 529547d1e..000000000 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffThumbnailGlideModule.kt +++ /dev/null @@ -1,99 +0,0 @@ -package deckers.thibault.aves.decoder - -import android.content.Context -import android.net.Uri -import com.bumptech.glide.Glide -import com.bumptech.glide.Priority -import com.bumptech.glide.Registry -import com.bumptech.glide.annotation.GlideModule -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.Options -import com.bumptech.glide.load.data.DataFetcher -import com.bumptech.glide.load.data.DataFetcher.DataCallback -import com.bumptech.glide.load.model.ModelLoader -import com.bumptech.glide.load.model.ModelLoaderFactory -import com.bumptech.glide.load.model.MultiModelLoaderFactory -import com.bumptech.glide.module.LibraryGlideModule -import com.bumptech.glide.signature.ObjectKey -import deckers.thibault.aves.utils.BitmapUtils.getBytes -import org.beyka.tiffbitmapfactory.TiffBitmapFactory -import java.io.InputStream - -@GlideModule -class TiffThumbnailGlideModule : LibraryGlideModule() { - override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - registry.append(TiffThumbnail::class.java, InputStream::class.java, TiffThumbnailLoader.Factory()) - } -} - -class TiffThumbnail(val context: Context, val uri: Uri, val page: Int) - -internal class TiffThumbnailLoader : ModelLoader { - override fun buildLoadData(model: TiffThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData { - return ModelLoader.LoadData(ObjectKey(model.uri), TiffThumbnailFetcher(model, width, height)) - } - - override fun handles(model: TiffThumbnail): Boolean = true - - internal class Factory : ModelLoaderFactory { - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = TiffThumbnailLoader() - - override fun teardown() {} - } -} - -internal class TiffThumbnailFetcher(val model: TiffThumbnail, val width: Int, val height: Int) : DataFetcher { - override fun loadData(priority: Priority, callback: DataCallback) { - val context = model.context - val uri = model.uri - val page = model.page - - // determine sample size - var fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() - if (fd == null) { - callback.onLoadFailed(Exception("null file descriptor")) - return - } - var sampleSize = 1 - var options = TiffBitmapFactory.Options().apply { - inJustDecodeBounds = true - inDirectoryNumber = page - } - TiffBitmapFactory.decodeFileDescriptor(fd, options) - val imageWidth = options.outWidth - val imageHeight = options.outHeight - if (imageHeight > height || imageWidth > width) { - while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) { - sampleSize *= 2 - } - } - - // decode - fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() - if (fd == null) { - callback.onLoadFailed(Exception("null file descriptor")) - return - } - options = TiffBitmapFactory.Options().apply { - inJustDecodeBounds = false - inDirectoryNumber = page - inSampleSize = sampleSize - } - val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) - if (bitmap == null) { - callback.onLoadFailed(Exception("null bitmap")) - } else { - callback.onDataReady(bitmap.getBytes()?.inputStream()) - } - } - - // already cleaned up in loadData and ByteArrayInputStream will be GC'd - override fun cleanup() {} - - // cannot cancel - override fun cancel() {} - - override fun getDataClass(): Class = InputStream::class.java - - override fun getDataSource(): DataSource = DataSource.LOCAL -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt index e942c2722..dfa0cbd32 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt @@ -6,8 +6,10 @@ import deckers.thibault.aves.model.provider.FieldMap class AvesEntry(map: FieldMap) { val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI val path = map["path"] as String? // best effort to get local path + val pageId = map["pageId"] as Int? // null means the main entry val mimeType = map["mimeType"] as String val width = map["width"] as Int val height = map["height"] as Int val rotationDegrees = map["rotationDegrees"] as Int + val isFlipped = map["isFlipped"] as Boolean } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index fd8421607..49d944b59 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -36,6 +36,10 @@ abstract class ImageProvider { callback.onFailure(UnsupportedOperationException()) } + open suspend fun exportMultiple(context: Context, mimeType: String, destinationDir: String, entries: List, callback: ImageOpCallback) { + callback.onFailure(UnsupportedOperationException()) + } + suspend fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) { val oldFile = File(oldPath) val newFile = File(oldFile.parent, newFilename) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index f06b5ea04..fc715a876 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -3,13 +3,21 @@ package deckers.thibault.aves.model.provider import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context +import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.provider.MediaStore import android.util.Log +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions import com.commonsware.cwac.document.DocumentFileCompat +import deckers.thibault.aves.decoder.MultiTrackImage +import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.SourceEntry +import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isImage @@ -311,6 +319,145 @@ class MediaStoreImageProvider : ImageProvider() { } } + override suspend fun exportMultiple( + context: Context, + mimeType: String, + destinationDir: String, + entries: List, + callback: ImageOpCallback, + ) { + val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir) + if (destinationDirDocFile == null) { + callback.onFailure(Exception("failed to create directory at path=$destinationDir")) + return + } + + for (entry in entries) { + val sourceUri = entry.uri + val sourcePath = entry.path + val pageId = entry.pageId + + val result = hashMapOf( + "uri" to sourceUri.toString(), + "pageId" to pageId, + "success" to false, + ) + + if (sourcePath != null) { + try { + val newFields = exportSingleByTreeDocAndScan( + context = context, + sourceEntry = entry, + destinationDir = destinationDir, + destinationDirDocFile = destinationDirDocFile, + exportMimeType = mimeType, + ) + result["newFields"] = newFields + result["success"] = true + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to export to destinationDir=$destinationDir entry with sourcePath=$sourcePath pageId=$pageId", e) + } + } + callback.onSuccess(result) + } + } + + private suspend fun exportSingleByTreeDocAndScan( + context: Context, + sourceEntry: AvesEntry, + destinationDir: String, + destinationDirDocFile: DocumentFileCompat, + exportMimeType: String, + ): FieldMap { + val sourceMimeType = sourceEntry.mimeType + val sourcePath = sourceEntry.path ?: throw Exception("source path is missing") + val sourceFile = File(sourcePath) + val pageId = sourceEntry.pageId + + val sourceFileName = sourceFile.name + var desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "") + if (pageId != null) { + val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId + desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" + } + val desiredFileName = desiredNameWithoutExtension + when (exportMimeType) { + MimeTypes.JPEG -> ".jpg" + MimeTypes.PNG -> ".png" + MimeTypes.WEBP -> ".webp" + else -> throw Exception("unsupported export MIME type=$exportMimeType") + } + + if (File(destinationDir, desiredFileName).exists()) { + throw Exception("file with name=$desiredFileName already exists in destination directory") + } + + // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` + // but in order to open an output stream to it, we need to use a `SingleDocumentFile` + // through a document URI, not a tree URI + // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first + @Suppress("BlockingMethodInNonBlockingContext") + val destinationTreeFile = destinationDirDocFile.createFile(exportMimeType, desiredNameWithoutExtension) + val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) + + val sourceUri = sourceEntry.uri + val model: Any = if (MimeTypes.isHeifLike(sourceMimeType) && pageId != null) { + MultiTrackImage(context, sourceUri, pageId) + } else if (sourceMimeType == MimeTypes.TIFF) { + TiffImage(context, sourceUri, pageId) + } else { + sourceUri + } + + // request a fresh image with the highest quality format + val glideOptions = RequestOptions() + .format(DecodeFormat.PREFER_ARGB_8888) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + + val target = Glide.with(context) + .asBitmap() + .apply(glideOptions) + .load(model) + .submit() + try { + @Suppress("BlockingMethodInNonBlockingContext") + var bitmap = target.get() + if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { + bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) + } + bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId") + + val quality = 100 + val format = when (exportMimeType) { + MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG + MimeTypes.PNG -> Bitmap.CompressFormat.PNG + MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (quality == 100) { + Bitmap.CompressFormat.WEBP_LOSSLESS + } else { + Bitmap.CompressFormat.WEBP_LOSSY + } + } else { + @Suppress("DEPRECATION") + Bitmap.CompressFormat.WEBP + } + else -> throw Exception("unsupported export MIME type=$exportMimeType") + } + + @Suppress("BlockingMethodInNonBlockingContext") + destinationDocFile.openOutputStream().use { + bitmap.compress(format, quality, it) + } + } finally { + Glide.with(context).clear(target) + } + + val fileName = destinationDocFile.name + val destinationFullPath = destinationDir + fileName + + return scanNewPath(context, destinationFullPath, exportMimeType) + } + companion object { private val LOG_TAG = LogUtils.createTag(MediaStoreImageProvider::class.java) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt index cb70f3347..2f71186ee 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt @@ -26,7 +26,7 @@ object BitmapUtils { } catch (e: IllegalStateException) { Log.e(LOG_TAG, "failed to get bytes from bitmap", e) } - return null; + return null } fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index cb41fd458..f033dbf22 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -11,8 +11,8 @@ object MimeTypes { const val HEIC = "image/heic" private const val HEIF = "image/heif" private const val ICO = "image/x-icon" - private const val JPEG = "image/jpeg" - private const val PNG = "image/png" + const val JPEG = "image/jpeg" + const val PNG = "image/png" const val TIFF = "image/tiff" private const val WBMP = "image/vnd.wap.wbmp" const val WEBP = "image/webp" diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 805df42f0..5cd6f62c3 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; enum EntryAction { delete, edit, + export, flip, info, open, @@ -31,6 +32,7 @@ class EntryActions { EntryAction.share, EntryAction.delete, EntryAction.rename, + EntryAction.export, EntryAction.print, EntryAction.viewSource, ]; @@ -52,6 +54,8 @@ extension ExtraEntryAction on EntryAction { return null; case EntryAction.delete: return 'Delete'; + case EntryAction.export: + return 'Export'; case EntryAction.info: return 'Info'; case EntryAction.rename: @@ -91,6 +95,8 @@ extension ExtraEntryAction on EntryAction { return null; case EntryAction.delete: return AIcons.delete; + case EntryAction.export: + return AIcons.export; case EntryAction.info: return AIcons.info; case EntryAction.rename: diff --git a/lib/model/actions/move_type.dart b/lib/model/actions/move_type.dart new file mode 100644 index 000000000..71b326b70 --- /dev/null +++ b/lib/model/actions/move_type.dart @@ -0,0 +1 @@ +enum MoveType { copy, move, export } diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 668d64ddc..ab7101131 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -96,13 +96,13 @@ class AvesEntry { return copied; } - AvesEntry getPageEntry(SinglePageInfo pageInfo) { + AvesEntry getPageEntry(SinglePageInfo pageInfo, {bool eraseDefaultPageId = true}) { if (pageInfo == null) return this; // do not provide the page ID for the default page, // so that we can treat this page like the main entry // and retrieve cached images for it - final pageId = pageInfo.isDefault ? null : pageInfo.pageId; + final pageId = eraseDefaultPageId && pageInfo.isDefault ? null : pageInfo.pageId; return AvesEntry( uri: uri, @@ -254,8 +254,6 @@ class AvesEntry { bool get canEdit => path != null; - bool get canPrint => !isVideo; - bool get canRotateAndFlip => canEdit && canEditExif; // support for writing EXIF @@ -637,9 +635,9 @@ class AvesEntry { // compare by: // 1) date descending - // 2) name ascending + // 2) name descending static int compareByDate(AvesEntry a, AvesEntry b) { final c = (b.bestDate ?? _epoch).compareTo(a.bestDate ?? _epoch); - return c != 0 ? c : compareByName(a, b); + return c != 0 ? c : -compareByName(a, b); } } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index da2e48710..c136ac722 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -1,15 +1,15 @@ import 'dart:async'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; -import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/image_op_events.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 8e94d0d59..31a67829b 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -5,10 +5,10 @@ import 'dart:typed_data'; 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/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:streams_channel/streams_channel.dart'; class ImageFileService { @@ -22,6 +22,7 @@ class ImageFileService { return { 'uri': entry.uri, 'path': entry.path, + 'pageId': entry.pageId, 'mimeType': entry.mimeType, 'width': entry.width, 'height': entry.height, @@ -236,7 +237,11 @@ class ImageFileService { } } - static Stream move(Iterable entries, {@required bool copy, @required String destinationAlbum}) { + static Stream move( + Iterable entries, { + @required bool copy, + @required String destinationAlbum, + }) { try { return opChannel.receiveBroadcastStream({ 'op': 'move', @@ -250,6 +255,24 @@ class ImageFileService { } } + static Stream export( + Iterable entries, { + String mimeType = MimeTypes.jpeg, + @required String destinationAlbum, + }) { + try { + return opChannel.receiveBroadcastStream({ + 'op': 'export', + 'entries': entries.map(_toPlatformEntryMap).toList(), + 'mimeType': mimeType, + 'destinationPath': destinationAlbum, + }).map((event) => ExportOpEvent.fromMap(event)); + } on PlatformException catch (e) { + debugPrint('export failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + return Stream.error(e); + } + } + static Future rename(AvesEntry entry, String newName) async { try { // return map with: 'contentId' 'path' 'title' 'uri' (all optional) @@ -292,57 +315,6 @@ class ImageFileService { } } -@immutable -class ImageOpEvent { - final bool success; - final String uri; - - const ImageOpEvent({ - this.success, - this.uri, - }); - - factory ImageOpEvent.fromMap(Map map) { - return ImageOpEvent( - success: map['success'] ?? false, - uri: map['uri'], - ); - } - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - return other is ImageOpEvent && other.success == success && other.uri == uri; - } - - @override - int get hashCode => hashValues(success, uri); - - @override - String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri}'; -} - -class MoveOpEvent extends ImageOpEvent { - final Map newFields; - - const MoveOpEvent({bool success, String uri, this.newFields}) - : super( - success: success, - uri: uri, - ); - - factory MoveOpEvent.fromMap(Map map) { - return MoveOpEvent( - success: map['success'] ?? false, - uri: map['uri'], - newFields: map['newFields'], - ); - } - - @override - String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri, newFields=$newFields}'; -} - // cf flutter/foundation `consolidateHttpClientResponseBytes` typedef BytesReceivedCallback = void Function(int cumulative, int total); diff --git a/lib/services/image_op_events.dart b/lib/services/image_op_events.dart new file mode 100644 index 000000000..2f30d8fe7 --- /dev/null +++ b/lib/services/image_op_events.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +@immutable +class ImageOpEvent { + final bool success; + final String uri; + + const ImageOpEvent({ + this.success, + this.uri, + }); + + factory ImageOpEvent.fromMap(Map map) { + return ImageOpEvent( + success: map['success'] ?? false, + uri: map['uri'], + ); + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is ImageOpEvent && other.success == success && other.uri == uri; + } + + @override + int get hashCode => hashValues(success, uri); + + @override + String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri}'; +} + +class MoveOpEvent extends ImageOpEvent { + final Map newFields; + + const MoveOpEvent({bool success, String uri, this.newFields}) + : super( + success: success, + uri: uri, + ); + + factory MoveOpEvent.fromMap(Map map) { + return MoveOpEvent( + success: map['success'] ?? false, + uri: map['uri'], + newFields: map['newFields'], + ); + } + + @override + String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri, newFields=$newFields}'; +} + +class ExportOpEvent extends MoveOpEvent { + final int pageId; + + const ExportOpEvent({bool success, String uri, this.pageId, Map newFields}) + : super( + success: success, + uri: uri, + newFields: newFields, + ); + + factory ExportOpEvent.fromMap(Map map) { + return ExportOpEvent( + success: map['success'] ?? false, + uri: map['uri'], + pageId: map['pageId'], + newFields: map['newFields'], + ); + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is ExportOpEvent && other.success == success && other.uri == uri && other.pageId == pageId; + } + + @override + int get hashCode => hashValues(success, uri, pageId); + + @override + String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri, pageId=$pageId, newFields=$newFields}'; +} diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 5cdf7f2b5..2ff083f0e 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -31,6 +31,7 @@ class AIcons { static const IconData createAlbum = Icons.add_circle_outline; static const IconData debug = Icons.whatshot_outlined; static const IconData delete = Icons.delete_outlined; + static const IconData export = Icons.save_alt_outlined; static const IconData flip = Icons.flip_outlined; static const IconData favourite = Icons.favorite_border; static const IconData favouriteActive = Icons.favorite; @@ -38,7 +39,7 @@ class AIcons { static const IconData group = Icons.group_work_outlined; static const IconData info = Icons.info_outlined; static const IconData layers = Icons.layers_outlined; - static const IconData openInNew = Icons.open_in_new_outlined; + static const IconData openOutside = Icons.open_in_new_outlined; static const IconData pin = Icons.push_pin_outlined; static const IconData print = Icons.print_outlined; static const IconData refresh = Icons.refresh_outlined; diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index b1f83b45a..36d96fcd2 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -2,11 +2,13 @@ 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/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_file_service.dart'; +import 'package:aves/services/image_op_events.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'; @@ -46,10 +48,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware void onCollectionActionSelected(BuildContext context, CollectionAction action) { switch (action) { case CollectionAction.copy: - _moveSelection(context, copy: true); + _moveSelection(context, moveType: MoveType.copy); break; case CollectionAction.move: - _moveSelection(context, copy: false); + _moveSelection(context, moveType: MoveType.move); break; case CollectionAction.refreshMetadata: source.refreshMetadata(selection); @@ -61,12 +63,12 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } } - Future _moveSelection(BuildContext context, {@required bool copy}) async { + Future _moveSelection(BuildContext context, {@required MoveType moveType}) async { final destinationAlbum = await Navigator.push( context, MaterialPageRoute( settings: RouteSettings(name: AlbumPickPage.routeName), - builder: (context) => AlbumPickPage(source: source, copy: copy), + builder: (context) => AlbumPickPage(source: source, moveType: moveType), ), ); if (destinationAlbum == null || destinationAlbum.isEmpty) return; @@ -74,8 +76,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (!await checkStoragePermission(context, selection)) return; - if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, copy)) return; + if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, moveType)) return; + final copy = moveType == MoveType.copy; showOpReport( context: context, selection: selection, diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 45e02fbed..1e7c2374e 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -1,5 +1,5 @@ import 'package:aves/model/entry.dart'; -import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/image_op_events.dart'; import 'package:aves/theme/durations.dart'; import 'package:flushbar/flushbar.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index 73ef17084..bf6e34b44 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; +import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; @@ -11,21 +12,30 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; mixin SizeAwareMixin { - Future checkFreeSpaceForMove(BuildContext context, Set selection, String destinationAlbum, bool copy) async { + Future checkFreeSpaceForMove( + BuildContext context, + Set selection, + String destinationAlbum, + MoveType moveType, + ) async { final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); final free = await AndroidFileService.getFreeSpace(destinationVolume); int needed; int sumSize(sum, entry) => sum + entry.sizeBytes; - if (copy) { - needed = selection.fold(0, sumSize); - } else { - // 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 otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); - 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)); - needed = max(fromOtherVolumes, largestSingle); + switch (moveType) { + case MoveType.copy: + case MoveType.export: + needed = selection.fold(0, sumSize); + 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 otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); + 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)); + needed = max(fromOtherVolumes, largestSingle); + break; } final hasEnoughSpace = needed < free; diff --git a/lib/widgets/common/basic/link_chip.dart b/lib/widgets/common/basic/link_chip.dart index c0f54a506..b04c0b2ae 100644 --- a/lib/widgets/common/basic/link_chip.dart +++ b/lib/widgets/common/basic/link_chip.dart @@ -44,7 +44,7 @@ class LinkChip extends StatelessWidget { SizedBox(width: 8), Builder( builder: (context) => Icon( - AIcons.openInNew, + AIcons.openOutside, size: DefaultTextStyle.of(context).style.fontSize, color: color, ), diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 6b33d36d9..a71522cde 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -1,4 +1,5 @@ import 'package:aves/model/actions/chip_actions.dart'; +import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -19,11 +20,11 @@ class AlbumPickPage extends StatefulWidget { static const routeName = '/album_pick'; final CollectionSource source; - final bool copy; + final MoveType moveType; const AlbumPickPage({ @required this.source, - @required this.copy, + @required this.moveType, }); @override @@ -38,7 +39,7 @@ class _AlbumPickPageState extends State { @override Widget build(BuildContext context) { Widget appBar = AlbumPickAppBar( - copy: widget.copy, + moveType: widget.moveType, actionDelegate: AlbumChipSetActionDelegate(source: source), queryNotifier: _queryNotifier, ); @@ -71,23 +72,36 @@ class _AlbumPickPageState extends State { } class AlbumPickAppBar extends StatelessWidget { - final bool copy; + final MoveType moveType; final AlbumChipSetActionDelegate actionDelegate; final ValueNotifier queryNotifier; static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight; const AlbumPickAppBar({ - @required this.copy, + @required this.moveType, @required this.actionDelegate, @required this.queryNotifier, }); @override Widget build(BuildContext context) { + String title() { + switch (moveType) { + case MoveType.copy: + return 'Copy to Album'; + case MoveType.export: + return 'Export to Album'; + case MoveType.move: + return 'Move to Album'; + default: + return null; + } + } + return SliverAppBar( leading: BackButton(), - title: Text(copy ? 'Copy to Album' : 'Move to Album'), + title: Text(title()), bottom: AlbumFilterBar( filterNotifier: queryNotifier, ), diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 15fec7a8c..5dfeba406 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -1,9 +1,11 @@ import 'package:aves/model/actions/chip_actions.dart'; +import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/image_op_events.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'; @@ -109,7 +111,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per final selection = source.rawEntries.where(filter.filter).toSet(); final destinationAlbum = path.join(path.dirname(album), newName); - if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, false)) return; + if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, MoveType.move)) return; showOpReport( context: context, diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index c18f3d9a2..d26324622 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -100,7 +100,6 @@ class ViewerDebugPage extends StatelessWidget { 'is360': '${entry.is360}', 'canEdit': '${entry.canEdit}', 'canEditExif': '${entry.canEditExif}', - 'canPrint': '${entry.canPrint}', 'canRotateAndFlip': '${entry.canRotateAndFlip}', 'xmpSubjects': '${entry.xmpSubjects}', }), diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index b2f298c8b..faaeec50b 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -1,23 +1,29 @@ 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/source/collection_lens.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/image_op_events.dart'; +import 'package:aves/services/metadata_service.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/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/printer.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; import 'package:pedantic/pedantic.dart'; -class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { +class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; final VoidCallback showInfo; @@ -36,6 +42,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { case EntryAction.delete: _showDeleteDialog(context, entry); break; + case EntryAction.export: + _showExportDialog(context, entry); + break; case EntryAction.info: showInfo(); break; @@ -140,6 +149,62 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { } } + Future _showExportDialog(BuildContext context, AvesEntry entry) async { + String destinationAlbum; + if (hasCollection) { + final source = collection.source; + destinationAlbum = await Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: AlbumPickPage.routeName), + builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export), + ), + ); + } else { + destinationAlbum = entry.directory; + } + + 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 (multiPageInfo.pageCount > 1) { + for (final page in multiPageInfo.pages) { + final pageEntry = entry.getPageEntry(page, eraseDefaultPageId: false); + selection.add(pageEntry); + } + } + } else { + selection.add(entry); + } + + showOpReport( + context: context, + selection: selection, + opStream: ImageFileService.export(selection, destinationAlbum: destinationAlbum), + onDone: (processed) { + final movedOps = processed.where((e) => e.success); + final movedCount = movedOps.length; + final selectionCount = selection.length; + if (movedCount < selectionCount) { + final count = selectionCount - movedCount; + showFeedback(context, 'Failed to export ${Intl.plural(count, one: '$count page', other: '$count pages')}'); + } else { + showFeedback(context, 'Done!'); + } + if (hasCollection) { + collection.source.refresh(); + } + }, + ); + } + Future _showRenameDialog(BuildContext context, AvesEntry entry) async { final newName = await showDialog( context: context, diff --git a/lib/widgets/viewer/info/maps/common.dart b/lib/widgets/viewer/info/maps/common.dart index d62506e1a..ee28ed748 100644 --- a/lib/widgets/viewer/info/maps/common.dart +++ b/lib/widgets/viewer/info/maps/common.dart @@ -63,7 +63,7 @@ class MapButtonPanel extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ MapOverlayButton( - icon: AIcons.openInNew, + icon: AIcons.openOutside, onPressed: () => AndroidAppService.openMap(geoUri).then((success) { if (!success) showNoMatchingAppDialog(context); }), diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index e2a026346..44fc7a3f1 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -111,8 +111,9 @@ class ViewerTopOverlay extends StatelessWidget { case EntryAction.rotateCW: case EntryAction.flip: return entry.canRotateAndFlip; + case EntryAction.export: case EntryAction.print: - return entry.canPrint; + return !entry.isVideo; case EntryAction.openMap: return entry.hasGps; case EntryAction.viewSource: @@ -194,14 +195,15 @@ class _TopOverlayRow extends StatelessWidget { onPressed: onPressed, ); break; - case EntryAction.info: - case EntryAction.share: case EntryAction.delete: + case EntryAction.export: + case EntryAction.flip: + case EntryAction.info: + case EntryAction.print: case EntryAction.rename: case EntryAction.rotateCCW: case EntryAction.rotateCW: - case EntryAction.flip: - case EntryAction.print: + case EntryAction.share: case EntryAction.viewSource: child = IconButton( icon: Icon(action.getIcon()), @@ -237,14 +239,15 @@ class _TopOverlayRow extends StatelessWidget { isMenuItem: true, ); break; - case EntryAction.info: - case EntryAction.share: case EntryAction.delete: + case EntryAction.export: + case EntryAction.flip: + case EntryAction.info: + case EntryAction.print: case EntryAction.rename: case EntryAction.rotateCCW: case EntryAction.rotateCW: - case EntryAction.flip: - case EntryAction.print: + case EntryAction.share: case EntryAction.viewSource: case EntryAction.debug: child = MenuRow(text: action.getText(), icon: action.getIcon()); diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index 3b0951e9f..59c1cd9d5 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -110,7 +110,7 @@ class _VideoControlOverlayState extends State with SingleTi OverlayButton( scale: scale, child: IconButton( - icon: Icon(AIcons.openInNew), + icon: Icon(AIcons.openOutside), onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype), tooltip: 'Open', ), From 218db5d0919e374f996db03910f640c9ba42f5bf Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 25 Jan 2021 12:43:04 +0900 Subject: [PATCH 22/44] export: support embedded images --- .../aves/model/provider/ImageProvider.kt | 158 ++++++++++++++++- .../model/provider/MediaStoreImageProvider.kt | 147 ---------------- lib/main.dart | 46 ++--- lib/model/source/collection_source.dart | 2 + lib/model/source/media_store_source.dart | 1 + lib/widgets/collection/grid/thumbnail.dart | 4 +- .../common/action_mixins/feedback.dart | 2 +- .../common/providers/settings_provider.dart | 17 -- lib/widgets/home_page.dart | 23 +-- lib/widgets/viewer/entry_action_delegate.dart | 27 ++- ...oller.dart => entry_horizontal_pager.dart} | 0 lib/widgets/viewer/entry_vertical_pager.dart | 165 ++++++++++++++++++ lib/widgets/viewer/entry_viewer_page.dart | 43 ++--- lib/widgets/viewer/entry_viewer_stack.dart | 159 +---------------- lib/widgets/viewer/info/info_page.dart | 58 ++++-- lib/widgets/viewer/info/info_search.dart | 29 ++- .../viewer/info/metadata/xmp_tile.dart | 12 +- lib/widgets/viewer/info/notifications.dart | 13 ++ 18 files changed, 468 insertions(+), 438 deletions(-) delete mode 100644 lib/widgets/common/providers/settings_provider.dart rename lib/widgets/viewer/{entry_scroller.dart => entry_horizontal_pager.dart} (100%) create mode 100644 lib/widgets/viewer/entry_vertical_pager.dart diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index 49d944b59..4ff6a1ff8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -2,18 +2,27 @@ package deckers.thibault.aves.model.provider import android.content.ContentUris import android.content.Context +import android.graphics.Bitmap import android.media.MediaScannerConnection import android.net.Uri +import android.os.Build import android.provider.MediaStore import android.util.Log import androidx.exifinterface.media.ExifInterface +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions import com.commonsware.cwac.document.DocumentFileCompat +import deckers.thibault.aves.decoder.MultiTrackImage +import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.ExifOrientationOp +import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.LogUtils -import deckers.thibault.aves.utils.MimeTypes.isImage -import deckers.thibault.aves.utils.MimeTypes.isVideo +import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp +import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import java.io.File import java.io.FileNotFoundException @@ -36,8 +45,145 @@ abstract class ImageProvider { callback.onFailure(UnsupportedOperationException()) } - open suspend fun exportMultiple(context: Context, mimeType: String, destinationDir: String, entries: List, callback: ImageOpCallback) { - callback.onFailure(UnsupportedOperationException()) + suspend fun exportMultiple( + context: Context, + mimeType: String, + destinationDir: String, + entries: List, + callback: ImageOpCallback, + ) { + val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir) + if (destinationDirDocFile == null) { + callback.onFailure(Exception("failed to create directory at path=$destinationDir")) + return + } + + for (entry in entries) { + val sourceUri = entry.uri + val sourcePath = entry.path + val pageId = entry.pageId + + val result = hashMapOf( + "uri" to sourceUri.toString(), + "pageId" to pageId, + "success" to false, + ) + + try { + val newFields = exportSingleByTreeDocAndScan( + context = context, + sourceEntry = entry, + destinationDir = destinationDir, + destinationDirDocFile = destinationDirDocFile, + exportMimeType = mimeType, + ) + result["newFields"] = newFields + result["success"] = true + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to export to destinationDir=$destinationDir entry with sourcePath=$sourcePath pageId=$pageId", e) + } + callback.onSuccess(result) + } + } + + private suspend fun exportSingleByTreeDocAndScan( + context: Context, + sourceEntry: AvesEntry, + destinationDir: String, + destinationDirDocFile: DocumentFileCompat, + exportMimeType: String, + ): FieldMap { + val sourceMimeType = sourceEntry.mimeType + val sourceUri = sourceEntry.uri + val pageId = sourceEntry.pageId + + var desiredNameWithoutExtension = if (sourceEntry.path != null) { + val sourcePath = sourceEntry.path + val sourceFile = File(sourcePath) + val sourceFileName = sourceFile.name + sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "") + } else { + sourceUri.lastPathSegment!! + } + if (pageId != null) { + val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId + desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" + } + val desiredFileName = desiredNameWithoutExtension + when (exportMimeType) { + MimeTypes.JPEG -> ".jpg" + MimeTypes.PNG -> ".png" + MimeTypes.WEBP -> ".webp" + else -> throw Exception("unsupported export MIME type=$exportMimeType") + } + + if (File(destinationDir, desiredFileName).exists()) { + throw Exception("file with name=$desiredFileName already exists in destination directory") + } + + // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` + // but in order to open an output stream to it, we need to use a `SingleDocumentFile` + // through a document URI, not a tree URI + // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first + @Suppress("BlockingMethodInNonBlockingContext") + val destinationTreeFile = destinationDirDocFile.createFile(exportMimeType, desiredNameWithoutExtension) + val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) + + val model: Any = if (MimeTypes.isHeifLike(sourceMimeType) && pageId != null) { + MultiTrackImage(context, sourceUri, pageId) + } else if (sourceMimeType == MimeTypes.TIFF) { + TiffImage(context, sourceUri, pageId) + } else { + sourceUri + } + + // request a fresh image with the highest quality format + val glideOptions = RequestOptions() + .format(DecodeFormat.PREFER_ARGB_8888) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + + val target = Glide.with(context) + .asBitmap() + .apply(glideOptions) + .load(model) + .submit() + try { + @Suppress("BlockingMethodInNonBlockingContext") + var bitmap = target.get() + if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { + bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) + } + bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId") + + val quality = 100 + val format = when (exportMimeType) { + MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG + MimeTypes.PNG -> Bitmap.CompressFormat.PNG + MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (quality == 100) { + Bitmap.CompressFormat.WEBP_LOSSLESS + } else { + Bitmap.CompressFormat.WEBP_LOSSY + } + } else { + @Suppress("DEPRECATION") + Bitmap.CompressFormat.WEBP + } + else -> throw Exception("unsupported export MIME type=$exportMimeType") + } + + @Suppress("BlockingMethodInNonBlockingContext") + destinationDocFile.openOutputStream().use { + bitmap.compress(format, quality, it) + } + } finally { + Glide.with(context).clear(target) + } + + val fileName = destinationDocFile.name + val destinationFullPath = destinationDir + fileName + + return scanNewPath(context, destinationFullPath, exportMimeType) } suspend fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) { @@ -151,9 +297,9 @@ abstract class ImageProvider { // `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872") // but we need an image/video media URI (e.g. "content://media/external/images/media/62872") contentId = ContentUris.parseId(newUri) - if (isImage(mimeType)) { + if (MimeTypes.isImage(mimeType)) { contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) - } else if (isVideo(mimeType)) { + } else if (MimeTypes.isVideo(mimeType)) { contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index fc715a876..f06b5ea04 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -3,21 +3,13 @@ package deckers.thibault.aves.model.provider import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context -import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.provider.MediaStore import android.util.Log -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DecodeFormat -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.RequestOptions import com.commonsware.cwac.document.DocumentFileCompat -import deckers.thibault.aves.decoder.MultiTrackImage -import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.SourceEntry -import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isImage @@ -319,145 +311,6 @@ class MediaStoreImageProvider : ImageProvider() { } } - override suspend fun exportMultiple( - context: Context, - mimeType: String, - destinationDir: String, - entries: List, - callback: ImageOpCallback, - ) { - val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir) - if (destinationDirDocFile == null) { - callback.onFailure(Exception("failed to create directory at path=$destinationDir")) - return - } - - for (entry in entries) { - val sourceUri = entry.uri - val sourcePath = entry.path - val pageId = entry.pageId - - val result = hashMapOf( - "uri" to sourceUri.toString(), - "pageId" to pageId, - "success" to false, - ) - - if (sourcePath != null) { - try { - val newFields = exportSingleByTreeDocAndScan( - context = context, - sourceEntry = entry, - destinationDir = destinationDir, - destinationDirDocFile = destinationDirDocFile, - exportMimeType = mimeType, - ) - result["newFields"] = newFields - result["success"] = true - } catch (e: Exception) { - Log.w(LOG_TAG, "failed to export to destinationDir=$destinationDir entry with sourcePath=$sourcePath pageId=$pageId", e) - } - } - callback.onSuccess(result) - } - } - - private suspend fun exportSingleByTreeDocAndScan( - context: Context, - sourceEntry: AvesEntry, - destinationDir: String, - destinationDirDocFile: DocumentFileCompat, - exportMimeType: String, - ): FieldMap { - val sourceMimeType = sourceEntry.mimeType - val sourcePath = sourceEntry.path ?: throw Exception("source path is missing") - val sourceFile = File(sourcePath) - val pageId = sourceEntry.pageId - - val sourceFileName = sourceFile.name - var desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "") - if (pageId != null) { - val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId - desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" - } - val desiredFileName = desiredNameWithoutExtension + when (exportMimeType) { - MimeTypes.JPEG -> ".jpg" - MimeTypes.PNG -> ".png" - MimeTypes.WEBP -> ".webp" - else -> throw Exception("unsupported export MIME type=$exportMimeType") - } - - if (File(destinationDir, desiredFileName).exists()) { - throw Exception("file with name=$desiredFileName already exists in destination directory") - } - - // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` - // but in order to open an output stream to it, we need to use a `SingleDocumentFile` - // through a document URI, not a tree URI - // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first - @Suppress("BlockingMethodInNonBlockingContext") - val destinationTreeFile = destinationDirDocFile.createFile(exportMimeType, desiredNameWithoutExtension) - val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) - - val sourceUri = sourceEntry.uri - val model: Any = if (MimeTypes.isHeifLike(sourceMimeType) && pageId != null) { - MultiTrackImage(context, sourceUri, pageId) - } else if (sourceMimeType == MimeTypes.TIFF) { - TiffImage(context, sourceUri, pageId) - } else { - sourceUri - } - - // request a fresh image with the highest quality format - val glideOptions = RequestOptions() - .format(DecodeFormat.PREFER_ARGB_8888) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) - - val target = Glide.with(context) - .asBitmap() - .apply(glideOptions) - .load(model) - .submit() - try { - @Suppress("BlockingMethodInNonBlockingContext") - var bitmap = target.get() - if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { - bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) - } - bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId") - - val quality = 100 - val format = when (exportMimeType) { - MimeTypes.JPEG -> Bitmap.CompressFormat.JPEG - MimeTypes.PNG -> Bitmap.CompressFormat.PNG - MimeTypes.WEBP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (quality == 100) { - Bitmap.CompressFormat.WEBP_LOSSLESS - } else { - Bitmap.CompressFormat.WEBP_LOSSY - } - } else { - @Suppress("DEPRECATION") - Bitmap.CompressFormat.WEBP - } - else -> throw Exception("unsupported export MIME type=$exportMimeType") - } - - @Suppress("BlockingMethodInNonBlockingContext") - destinationDocFile.openOutputStream().use { - bitmap.compress(format, quality, it) - } - } finally { - Glide.with(context).clear(target) - } - - val fileName = destinationDocFile.name - val destinationFullPath = destinationDir + fileName - - return scanNewPath(context, destinationFullPath, exportMimeType) - } - companion object { private val LOG_TAG = LogUtils.createTag(MediaStoreImageProvider::class.java) diff --git a/lib/main.dart b/lib/main.dart index 2445de98a..32927e63b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,10 +2,11 @@ import 'dart:isolate'; import 'dart:ui'; 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/theme/icons.dart'; import 'package:aves/widgets/common/behaviour/route_tracker.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; -import 'package:aves/widgets/common/providers/settings_provider.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/welcome_page.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; @@ -16,6 +17,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:overlay_support/overlay_support.dart'; +import 'package:provider/provider.dart'; void main() { // HttpClient.enableTimelineLogging = true; // enable network traffic logging @@ -137,25 +139,29 @@ class _AvesAppState extends State { Widget build(BuildContext context) { // place the settings provider above `MaterialApp` // so it can be used during navigation transitions - return SettingsProvider( - child: OverlaySupport( - child: FutureBuilder( - future: _appSetup, - builder: (context, snapshot) { - final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) - ? getFirstPage() - : Scaffold( - body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), - ); - return MaterialApp( - navigatorKey: _navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - title: 'Aves', - darkTheme: darkTheme, - themeMode: ThemeMode.dark, - ); - }, + return ChangeNotifierProvider.value( + value: settings, + child: Provider( + create: (context) => MediaStoreSource(), + child: OverlaySupport( + child: FutureBuilder( + future: _appSetup, + builder: (context, snapshot) { + final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) + ? getFirstPage() + : Scaffold( + body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), + ); + return MaterialApp( + navigatorKey: _navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + title: 'Aves', + darkTheme: darkTheme, + themeMode: ThemeMode.dark, + ); + }, + ), ), ), ); diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index c136ac722..c946af499 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -164,6 +164,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); } + Future init(); + Future refresh(); Future refreshMetadata(Set entries); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 611db5974..6f93289e1 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -13,6 +13,7 @@ import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:pedantic/pedantic.dart'; class MediaStoreSource extends CollectionSource { + @override Future init() async { final stopwatch = Stopwatch()..start(); stateNotifier.value = SourceState.loading; diff --git a/lib/widgets/collection/grid/thumbnail.dart b/lib/widgets/collection/grid/thumbnail.dart index c6b6a4d75..23d31c827 100644 --- a/lib/widgets/collection/grid/thumbnail.dart +++ b/lib/widgets/collection/grid/thumbnail.dart @@ -53,8 +53,8 @@ class InteractiveThumbnail extends StatelessWidget { Navigator.push( context, TransparentMaterialPageRoute( - settings: RouteSettings(name: MultiEntryViewerPage.routeName), - pageBuilder: (c, a, sa) => MultiEntryViewerPage( + settings: RouteSettings(name: EntryViewerPage.routeName), + pageBuilder: (c, a, sa) => EntryViewerPage( collection: collection, initialEntry: entry, ), diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 1e7c2374e..66156eae3 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -82,7 +82,7 @@ mixin FeedbackMixin { Future _hideOpReportOverlay() async { await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation); - _opReportOverlayEntry.remove(); + _opReportOverlayEntry?.remove(); _opReportOverlayEntry = null; } } diff --git a/lib/widgets/common/providers/settings_provider.dart b/lib/widgets/common/providers/settings_provider.dart deleted file mode 100644 index a47b5329b..000000000 --- a/lib/widgets/common/providers/settings_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:aves/model/settings/settings.dart'; -import 'package:flutter/widgets.dart'; -import 'package:provider/provider.dart'; - -class SettingsProvider extends StatelessWidget { - final Widget child; - - const SettingsProvider({@required this.child}); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: settings, - child: child, - ); - } -} diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 5d56a05b5..08346aa82 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -5,7 +5,7 @@ import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/model/source/media_store_source.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/utils/android_file_utils.dart'; @@ -20,6 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pedantic/pedantic.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; class HomePage extends StatefulWidget { static const routeName = '/'; @@ -34,7 +35,6 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - MediaStoreSource _mediaStore; AvesEntry _viewerEntry; String _shortcutRouteName; List _shortcutFilters; @@ -100,9 +100,9 @@ class _HomePageState extends State { unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString())); if (AvesApp.mode != AppMode.view) { - _mediaStore = MediaStoreSource(); - await _mediaStore.init(); - unawaited(_mediaStore.refresh()); + final source = context.read(); + await source.init(); + unawaited(source.refresh()); } unawaited(Navigator.pushReplacement(context, _getRedirectRoute())); @@ -121,8 +121,10 @@ class _HomePageState extends State { Route _getRedirectRoute() { if (AvesApp.mode == AppMode.view) { return DirectMaterialPageRoute( - settings: RouteSettings(name: SingleEntryViewerPage.routeName), - builder: (_) => SingleEntryViewerPage(entry: _viewerEntry), + settings: RouteSettings(name: EntryViewerPage.routeName), + builder: (_) => EntryViewerPage( + initialEntry: _viewerEntry, + ), ); } @@ -134,15 +136,16 @@ class _HomePageState extends State { routeName = _shortcutRouteName ?? settings.homePage.routeName; filters = (_shortcutFilters ?? []).map(CollectionFilter.fromJson); } + final source = context.read(); switch (routeName) { case AlbumListPage.routeName: return DirectMaterialPageRoute( settings: RouteSettings(name: AlbumListPage.routeName), - builder: (_) => AlbumListPage(source: _mediaStore), + builder: (_) => AlbumListPage(source: source), ); case SearchPage.routeName: return SearchPageRoute( - delegate: CollectionSearchDelegate(source: _mediaStore), + delegate: CollectionSearchDelegate(source: source), ); case CollectionPage.routeName: default: @@ -150,7 +153,7 @@ class _HomePageState extends State { settings: RouteSettings(name: CollectionPage.routeName), builder: (_) => CollectionPage( CollectionLens( - source: _mediaStore, + source: source, filters: filters, groupFactor: settings.collectionGroupFactor, sortFactor: settings.collectionSortFactor, diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index faaeec50b..d46f09280 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -4,6 +4,7 @@ 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/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_file_service.dart'; import 'package:aves/services/image_op_events.dart'; @@ -22,6 +23,7 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:pedantic/pedantic.dart'; +import 'package:provider/provider.dart'; class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; @@ -150,19 +152,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } Future _showExportDialog(BuildContext context, AvesEntry entry) async { - String destinationAlbum; - if (hasCollection) { - final source = collection.source; - destinationAlbum = await Navigator.push( - context, - MaterialPageRoute( - settings: RouteSettings(name: AlbumPickPage.routeName), - builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export), - ), - ); - } else { - destinationAlbum = entry.directory; - } + final source = context.read(); + final destinationAlbum = await Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: AlbumPickPage.routeName), + builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export), + ), + ); if (destinationAlbum == null || destinationAlbum.isEmpty) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; @@ -198,9 +195,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } else { showFeedback(context, 'Done!'); } - if (hasCollection) { - collection.source.refresh(); - } + source.refresh(); }, ); } diff --git a/lib/widgets/viewer/entry_scroller.dart b/lib/widgets/viewer/entry_horizontal_pager.dart similarity index 100% rename from lib/widgets/viewer/entry_scroller.dart rename to lib/widgets/viewer/entry_horizontal_pager.dart diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart new file mode 100644 index 000000000..95391d6a9 --- /dev/null +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -0,0 +1,165 @@ +import 'dart:math'; + +import 'package:aves/model/entry.dart'; +import 'package:aves/model/source/collection_lens.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'; +import 'package:aves/widgets/viewer/info/notifications.dart'; +import 'package:aves/widgets/viewer/multipage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; +import 'package:tuple/tuple.dart'; + +class ViewerVerticalPageView extends StatefulWidget { + final CollectionLens collection; + final ValueNotifier entryNotifier; + final List> videoControllers; + final List> multiPageControllers; + final PageController horizontalPager, verticalPager; + final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; + final VoidCallback onImageTap, onImagePageRequested; + final void Function(String uri) onViewDisposed; + + const ViewerVerticalPageView({ + @required this.collection, + @required this.entryNotifier, + @required this.videoControllers, + @required this.multiPageControllers, + @required this.verticalPager, + @required this.horizontalPager, + @required this.onVerticalPageChanged, + @required this.onHorizontalPageChanged, + @required this.onImageTap, + @required this.onImagePageRequested, + @required this.onViewDisposed, + }); + + @override + _ViewerVerticalPageViewState createState() => _ViewerVerticalPageViewState(); +} + +class _ViewerVerticalPageViewState extends State { + final ValueNotifier _backgroundColorNotifier = ValueNotifier(Colors.black); + final ValueNotifier _infoPageVisibleNotifier = ValueNotifier(false); + AvesEntry _oldEntry; + + CollectionLens get collection => widget.collection; + + bool get hasCollection => collection != null; + + AvesEntry get entry => widget.entryNotifier.value; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant ViewerVerticalPageView oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(ViewerVerticalPageView widget) { + widget.verticalPager.addListener(_onVerticalPageControllerChanged); + widget.entryNotifier.addListener(_onEntryChanged); + if (_oldEntry != entry) _onEntryChanged(); + } + + void _unregisterWidget(ViewerVerticalPageView widget) { + widget.verticalPager.removeListener(_onVerticalPageControllerChanged); + widget.entryNotifier.removeListener(_onEntryChanged); + _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, + onTap: widget.onImageTap, + onPageChanged: widget.onHorizontalPageChanged, + videoControllers: widget.videoControllers, + multiPageControllers: widget.multiPageControllers, + onViewDisposed: widget.onViewDisposed, + ) + : SingleEntryScroller( + entry: entry, + onTap: widget.onImageTap, + videoControllers: widget.videoControllers, + multiPageControllers: widget.multiPageControllers, + ), + NotificationListener( + onNotification: (notification) { + if (notification is BackUpNotification) widget.onImagePageRequested(); + return false; + }, + child: InfoPage( + collection: collection, + entryNotifier: widget.entryNotifier, + visibleNotifier: _infoPageVisibleNotifier, + ), + ), + ]; + return ValueListenableBuilder( + valueListenable: _backgroundColorNotifier, + builder: (context, backgroundColor, child) => Container( + color: backgroundColor, + child: child, + ), + child: PageView( + key: Key('vertical-pageview'), + scrollDirection: Axis.vertical, + controller: widget.verticalPager, + physics: MagnifierScrollerPhysics(parent: PageScrollPhysics()), + onPageChanged: (page) { + widget.onVerticalPageChanged(page); + _infoPageVisibleNotifier.value = page == pages.length - 1; + }, + children: pages, + ), + ); + } + + void _onVerticalPageControllerChanged() { + final opacity = min(1.0, widget.verticalPager.page); + _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity); + } + + // when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted) + void _onEntryChanged() { + _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); + _oldEntry = entry; + + if (entry != null) { + entry.imageChangeNotifier.addListener(_onImageChanged); + // make sure to locate the entry, + // so that we can display the address instead of coordinates + // even when background locating has not reached this entry yet + entry.locate(); + } else { + Navigator.pop(context); + } + } + + // when the entry image itself changed (e.g. after rotation) + void _onImageChanged() async { + // rebuild to refresh the Image inside ImagePage + setState(() {}); + } +} diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 5ee08cdd0..8bb27913c 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -4,50 +4,33 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:flutter/material.dart'; -class MultiEntryViewerPage extends AnimatedWidget { +class EntryViewerPage extends StatelessWidget { static const routeName = '/viewer'; final CollectionLens collection; final AvesEntry initialEntry; - const MultiEntryViewerPage({ + const EntryViewerPage({ Key key, this.collection, this.initialEntry, - }) : super(key: key, listenable: collection); - - @override - Widget build(BuildContext context) { - return MediaQueryDataProvider( - child: Scaffold( - body: EntryViewerStack( - collection: collection, - initialEntry: initialEntry, - ), - backgroundColor: Colors.transparent, - resizeToAvoidBottomInset: false, - ), - ); - } -} - -class SingleEntryViewerPage extends StatelessWidget { - static const routeName = '/viewer'; - - final AvesEntry entry; - - const SingleEntryViewerPage({ - Key key, - this.entry, }) : super(key: key); @override Widget build(BuildContext context) { return MediaQueryDataProvider( child: Scaffold( - body: EntryViewerStack( - initialEntry: entry, - ), + body: collection != null + ? AnimatedBuilder( + animation: collection, + builder: (context, child) => EntryViewerStack( + collection: collection, + initialEntry: initialEntry, + ), + ) + : EntryViewerStack( + initialEntry: initialEntry, + ), backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black, resizeToAvoidBottomInset: false, ), diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 4ff471002..b9867800f 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -9,10 +9,8 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/insets.dart'; -import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; import 'package:aves/widgets/viewer/entry_action_delegate.dart'; -import 'package:aves/widgets/viewer/entry_scroller.dart'; -import 'package:aves/widgets/viewer/info/info_page.dart'; +import 'package:aves/widgets/viewer/entry_vertical_pager.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/overlay/bottom.dart'; @@ -36,7 +34,7 @@ class EntryViewerStack extends StatefulWidget { const EntryViewerStack({ Key key, this.collection, - this.initialEntry, + @required this.initialEntry, }) : super(key: key); @override @@ -475,154 +473,3 @@ class _EntryViewerStackState extends State with SingleTickerPr void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause()); } - -class ViewerVerticalPageView extends StatefulWidget { - final CollectionLens collection; - final ValueNotifier entryNotifier; - final List> videoControllers; - final List> multiPageControllers; - final PageController horizontalPager, verticalPager; - final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; - final VoidCallback onImageTap, onImagePageRequested; - final void Function(String uri) onViewDisposed; - - const ViewerVerticalPageView({ - @required this.collection, - @required this.entryNotifier, - @required this.videoControllers, - @required this.multiPageControllers, - @required this.verticalPager, - @required this.horizontalPager, - @required this.onVerticalPageChanged, - @required this.onHorizontalPageChanged, - @required this.onImageTap, - @required this.onImagePageRequested, - @required this.onViewDisposed, - }); - - @override - _ViewerVerticalPageViewState createState() => _ViewerVerticalPageViewState(); -} - -class _ViewerVerticalPageViewState extends State { - final ValueNotifier _backgroundColorNotifier = ValueNotifier(Colors.black); - final ValueNotifier _infoPageVisibleNotifier = ValueNotifier(false); - AvesEntry _oldEntry; - - CollectionLens get collection => widget.collection; - - bool get hasCollection => collection != null; - - AvesEntry get entry => widget.entryNotifier.value; - - @override - void initState() { - super.initState(); - _registerWidget(widget); - } - - @override - void didUpdateWidget(covariant ViewerVerticalPageView oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - } - - @override - void dispose() { - _unregisterWidget(widget); - super.dispose(); - } - - void _registerWidget(ViewerVerticalPageView widget) { - widget.verticalPager.addListener(_onVerticalPageControllerChanged); - widget.entryNotifier.addListener(_onEntryChanged); - if (_oldEntry != entry) _onEntryChanged(); - } - - void _unregisterWidget(ViewerVerticalPageView widget) { - widget.verticalPager.removeListener(_onVerticalPageControllerChanged); - widget.entryNotifier.removeListener(_onEntryChanged); - _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, - onTap: widget.onImageTap, - onPageChanged: widget.onHorizontalPageChanged, - videoControllers: widget.videoControllers, - multiPageControllers: widget.multiPageControllers, - onViewDisposed: widget.onViewDisposed, - ) - : SingleEntryScroller( - entry: entry, - onTap: widget.onImageTap, - videoControllers: widget.videoControllers, - multiPageControllers: widget.multiPageControllers, - ), - NotificationListener( - onNotification: (notification) { - if (notification is BackUpNotification) widget.onImagePageRequested(); - return false; - }, - child: InfoPage( - collection: collection, - entryNotifier: widget.entryNotifier, - visibleNotifier: _infoPageVisibleNotifier, - ), - ), - ]; - return ValueListenableBuilder( - valueListenable: _backgroundColorNotifier, - builder: (context, backgroundColor, child) => Container( - color: backgroundColor, - child: child, - ), - child: PageView( - key: Key('vertical-pageview'), - scrollDirection: Axis.vertical, - controller: widget.verticalPager, - physics: MagnifierScrollerPhysics(parent: PageScrollPhysics()), - onPageChanged: (page) { - widget.onVerticalPageChanged(page); - _infoPageVisibleNotifier.value = page == pages.length - 1; - }, - children: pages, - ), - ); - } - - void _onVerticalPageControllerChanged() { - final opacity = min(1.0, widget.verticalPager.page); - _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity); - } - - // when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted) - void _onEntryChanged() { - _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); - _oldEntry = entry; - - if (entry != null) { - entry.imageChangeNotifier.addListener(_onImageChanged); - // make sure to locate the entry, - // so that we can display the address instead of coordinates - // even when background locating has not reached this entry yet - entry.locate(); - } else { - Navigator.pop(context); - } - } - - // when the entry image itself changed (e.g. after rotation) - void _onImageChanged() async { - // rebuild to refresh the Image inside ImagePage - setState(() {}); - } -} diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 87b9afe34..b84e2a4d7 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -1,9 +1,11 @@ -import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/info/basic_section.dart'; import 'package:aves/widgets/viewer/info/info_app_bar.dart'; import 'package:aves/widgets/viewer/info/location_section.dart'; @@ -45,25 +47,31 @@ class _InfoPageState extends State { bottom: false, child: NotificationListener( onNotification: _handleTopScroll, - child: Selector( - selector: (c, mq) => mq.size.width, - builder: (c, mqWidth, child) { - return ValueListenableBuilder( - valueListenable: widget.entryNotifier, - builder: (context, entry, child) { - return entry != null - ? _InfoPageContent( - collection: collection, - entry: entry, - visibleNotifier: widget.visibleNotifier, - scrollController: _scrollController, - split: mqWidth > 400, - goToViewer: _goToViewer, - ) - : SizedBox.shrink(); - }, - ); + child: NotificationListener( + onNotification: (notification) { + _openTempEntry(notification.entry); + return true; }, + child: Selector( + selector: (c, mq) => mq.size.width, + builder: (c, mqWidth, child) { + return ValueListenableBuilder( + valueListenable: widget.entryNotifier, + builder: (context, entry, child) { + return entry != null + ? _InfoPageContent( + collection: collection, + entry: entry, + visibleNotifier: widget.visibleNotifier, + scrollController: _scrollController, + split: mqWidth > 400, + goToViewer: _goToViewer, + ) + : SizedBox.shrink(); + }, + ); + }, + ), ), ), ), @@ -103,6 +111,18 @@ class _InfoPageState extends State { curve: Curves.easeInOut, ); } + + void _openTempEntry(AvesEntry tempEntry) { + Navigator.push( + context, + TransparentMaterialPageRoute( + settings: RouteSettings(name: EntryViewerPage.routeName), + pageBuilder: (c, a, sa) => EntryViewerPage( + initialEntry: tempEntry, + ), + ), + ); + } } class _InfoPageContent extends StatefulWidget { diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index 04744e277..601f6b70c 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -1,8 +1,11 @@ import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; +import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -109,10 +112,28 @@ class InfoSearchDelegate extends SearchDelegate { icon: AIcons.info, text: 'No matching keys', ) - : ListView.builder( - padding: EdgeInsets.all(8), - itemBuilder: (context, index) => tiles[index], - itemCount: tiles.length, + : NotificationListener( + onNotification: (notification) { + _openTempEntry(context, notification.entry); + return true; + }, + child: ListView.builder( + padding: EdgeInsets.all(8), + itemBuilder: (context, index) => tiles[index], + itemCount: tiles.length, + ), ); } + + void _openTempEntry(BuildContext context, AvesEntry tempEntry) { + Navigator.push( + context, + TransparentMaterialPageRoute( + settings: RouteSettings(name: EntryViewerPage.routeName), + pageBuilder: (c, a, sa) => EntryViewerPage( + initialEntry: tempEntry, + ), + ), + ); + } } diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 62098f1f3..731d49867 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -6,10 +6,8 @@ import 'package:aves/ref/xmp.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; -import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; -import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart'; @@ -17,6 +15,7 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart'; +import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:pedantic/pedantic.dart'; @@ -123,13 +122,6 @@ class _XmpDirTileState extends State with FeedbackMixin { return; } - final embedEntry = AvesEntry.fromMap(fields); - unawaited(Navigator.push( - context, - TransparentMaterialPageRoute( - settings: RouteSettings(name: SingleEntryViewerPage.routeName), - pageBuilder: (c, a, sa) => SingleEntryViewerPage(entry: embedEntry), - ), - )); + OpenTempEntryNotification(entry: AvesEntry.fromMap(fields)).dispatch(context); } } diff --git a/lib/widgets/viewer/info/notifications.dart b/lib/widgets/viewer/info/notifications.dart index 0f46e5aac..32afe7ae9 100644 --- a/lib/widgets/viewer/info/notifications.dart +++ b/lib/widgets/viewer/info/notifications.dart @@ -1,4 +1,6 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class BackUpNotification extends Notification {} @@ -8,3 +10,14 @@ class FilterNotification extends Notification { const FilterNotification(this.filter); } + +class OpenTempEntryNotification extends Notification { + final AvesEntry entry; + + const OpenTempEntryNotification({ + @required this.entry, + }); + + @override + String toString() => '$runtimeType#${shortHash(this)}{entry=$entry}'; +} From c332e125bbc50e7e8b8b9c077a2dcc9d6bb874c5 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 25 Jan 2021 14:38:07 +0900 Subject: [PATCH 23/44] export: support view mode --- lib/model/source/collection_source.dart | 2 + lib/model/source/media_store_source.dart | 6 ++ lib/widgets/filter_grids/album_pick.dart | 78 +++++++++++++------ .../common/chip_set_action_delegate.dart | 2 +- lib/widgets/viewer/entry_action_delegate.dart | 4 + 5 files changed, 66 insertions(+), 26 deletions(-) diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index c946af499..37140c22a 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -164,6 +164,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); } + bool get initialized => false; + Future init(); Future refresh(); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 6f93289e1..f21ab63ab 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -13,6 +13,11 @@ import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:pedantic/pedantic.dart'; class MediaStoreSource extends CollectionSource { + bool _initialized = false; + + @override + bool get initialized => _initialized; + @override Future init() async { final stopwatch = Stopwatch()..start(); @@ -29,6 +34,7 @@ class MediaStoreSource extends CollectionSource { settings.catalogTimeZone = currentTimeZone; } await loadDates(); // 100ms for 5400 entries + _initialized = true; debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}'); } diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index a71522cde..edb98101d 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -2,10 +2,14 @@ import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/app_bar_subtitle.dart'; +import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/dialogs/create_album_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -13,8 +17,10 @@ import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class AlbumPickPage extends StatefulWidget { static const routeName = '/album_pick'; @@ -39,32 +45,36 @@ class _AlbumPickPageState extends State { @override Widget build(BuildContext context) { Widget appBar = AlbumPickAppBar( + source: source, moveType: widget.moveType, actionDelegate: AlbumChipSetActionDelegate(source: source), queryNotifier: _queryNotifier, ); - return Selector( - selector: (context, s) => s.albumSortFactor, - builder: (context, sortFactor, child) { - return FilterGridPage( - source: source, - appBar: appBar, - filterSections: AlbumListPage.getAlbumEntries(source), - showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, - applyQuery: (filters, query) { - if (query == null || query.isEmpty) return filters; - query = query.toUpperCase(); - return filters.where((item) => item.filter.uniqueName.toUpperCase().contains(query)).toList(); - }, - queryNotifier: _queryNotifier, - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: 'No albums', + return Selector>( + selector: (context, s) => Tuple2(s.albumGroupFactor, s.albumSortFactor), + builder: (context, s, child) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterGridPage( + source: source, + appBar: appBar, + filterSections: AlbumListPage.getAlbumEntries(source), + showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, + applyQuery: (filters, query) { + if (query == null || query.isEmpty) return filters; + query = query.toUpperCase(); + return filters.where((item) => item.filter.uniqueName.toUpperCase().contains(query)).toList(); + }, + queryNotifier: _queryNotifier, + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: 'No albums', + ), + settingsRouteKey: AlbumListPage.routeName, + appBarHeight: AlbumPickAppBar.preferredHeight, + onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), ), - settingsRouteKey: AlbumListPage.routeName, - appBarHeight: AlbumPickAppBar.preferredHeight, - onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), ); }, ); @@ -72,6 +82,7 @@ class _AlbumPickPageState extends State { } class AlbumPickAppBar extends StatelessWidget { + final CollectionSource source; final MoveType moveType; final AlbumChipSetActionDelegate actionDelegate; final ValueNotifier queryNotifier; @@ -79,6 +90,7 @@ class AlbumPickAppBar extends StatelessWidget { static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight; const AlbumPickAppBar({ + @required this.source, @required this.moveType, @required this.actionDelegate, @required this.queryNotifier, @@ -101,7 +113,10 @@ class AlbumPickAppBar extends StatelessWidget { return SliverAppBar( leading: BackButton(), - title: Text(title()), + title: SourceStateAwareAppBarTitle( + title: Text(title()), + source: source, + ), bottom: AlbumFilterBar( filterNotifier: queryNotifier, ), @@ -119,10 +134,23 @@ class AlbumPickAppBar extends StatelessWidget { }, tooltip: 'Create album', ), - IconButton( - icon: Icon(AIcons.sort), - onPressed: () => actionDelegate.onActionSelected(context, ChipSetAction.sort), - tooltip: 'Sort…', + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + value: ChipSetAction.sort, + child: MenuRow(text: 'Sort…', icon: AIcons.sort), + ), + PopupMenuItem( + value: ChipSetAction.group, + child: MenuRow(text: 'Group…', icon: AIcons.group), + ), + ]; + }, + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => actionDelegate.onActionSelected(context, action)); + }, ), ], floating: true, 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 08e9e32d0..2625b42cb 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -92,7 +92,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate { builder: (context) => AvesSelectionDialog( initialValue: settings.albumGroupFactor, options: { - AlbumChipGroupFactor.importance: 'By importance', + AlbumChipGroupFactor.importance: 'By tier', AlbumChipGroupFactor.volume: 'By storage volume', AlbumChipGroupFactor.none: 'Do not group', }, diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index d46f09280..d81211a9e 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -153,6 +153,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix Future _showExportDialog(BuildContext context, AvesEntry entry) async { final source = context.read(); + if (!source.initialized) { + await source.init(); + unawaited(source.refresh()); + } final destinationAlbum = await Navigator.push( context, MaterialPageRoute( From b59b323d34c81ca36b889203b04e81f8b6f591a3 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 25 Jan 2021 18:21:18 +0900 Subject: [PATCH 24/44] multipage: heic track tiling --- .../aves/channel/calls/ImageFileHandler.kt | 24 ++++---- .../calls/{ => fetchers}/RegionFetcher.kt | 57 ++++++++++++++++++- .../calls/{ => fetchers}/ThumbnailFetcher.kt | 2 +- .../calls/{ => fetchers}/TiffRegionFetcher.kt | 4 +- lib/model/entry.dart | 3 +- 5 files changed, 73 insertions(+), 17 deletions(-) rename android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/{ => fetchers}/RegionFetcher.kt (58%) rename android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/{ => fetchers}/ThumbnailFetcher.kt (99%) rename android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/{ => fetchers}/TiffRegionFetcher.kt (95%) 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 43a8ff81d..f37b4f2ce 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 @@ -5,6 +5,9 @@ import android.graphics.Rect import android.net.Uri import android.util.Size import com.bumptech.glide.Glide +import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher +import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher +import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.provider.FieldMap import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback @@ -102,19 +105,20 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { val regionRect = Rect(x, y, x + width, y + height) when (mimeType) { MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch( - uri, - sampleSize, - regionRect, + uri = uri, page = pageId ?: 0, - result, + sampleSize = sampleSize, + regionRect = regionRect, + result = result, ) else -> regionFetcher.fetch( - uri, - mimeType, - sampleSize, - regionRect, - Size(imageWidth, imageHeight), - result, + uri = uri, + mimeType = mimeType, + pageId = pageId, + sampleSize = sampleSize, + regionRect = regionRect, + imageSize = Size(imageWidth, imageHeight), + result = result, ) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt similarity index 58% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt index 7b9689a7e..e432fac28 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt @@ -1,15 +1,22 @@ -package deckers.thibault.aves.channel.calls +package deckers.thibault.aves.channel.calls.fetchers import android.content.Context +import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.BitmapRegionDecoder import android.graphics.Rect import android.net.Uri import android.util.Size +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions +import deckers.thibault.aves.decoder.MultiTrackImage import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.MethodChannel +import java.io.File import kotlin.math.roundToInt class RegionFetcher internal constructor( @@ -17,21 +24,42 @@ class RegionFetcher internal constructor( ) { private var lastDecoderRef: LastDecoderRef? = null + private val pageTempUris = HashMap, Uri>() + + private val multiTrackGlideOptions = RequestOptions() + .format(DecodeFormat.PREFER_ARGB_8888) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + fun fetch( uri: Uri, mimeType: String, + pageId: Int?, sampleSize: Int, regionRect: Rect, imageSize: Size, result: MethodChannel.Result, ) { + if (MimeTypes.isHeifLike(mimeType) && pageId != null) { + val id = Pair(uri, pageId) + fetch( + uri = pageTempUris.getOrPut(id) { createJpegForPage(uri, pageId) }, + mimeType = MimeTypes.JPEG, + pageId = null, + sampleSize = sampleSize, + regionRect = regionRect, + imageSize = imageSize, + result = result, + ) + return + } + val options = BitmapFactory.Options().apply { inSampleSize = sampleSize } var currentDecoderRef = lastDecoderRef if (currentDecoderRef != null && currentDecoderRef.uri != uri) { - currentDecoderRef.decoder.recycle() currentDecoderRef = null } @@ -74,6 +102,31 @@ class RegionFetcher internal constructor( result.error("getRegion-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message) } } + + private fun createJpegForPage(sourceUri: Uri, pageId: Int): Uri { + val target = Glide.with(context) + .asBitmap() + .apply(multiTrackGlideOptions) + .load(MultiTrackImage(context, sourceUri, pageId)) + .submit() + try { + val bitmap = target.get() +// if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { +// bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) +// } + bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId") + + val tempFile = File.createTempFile("aves", null, context.cacheDir).apply { + deleteOnExit() + outputStream().use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + } + } + return Uri.fromFile(tempFile) + } finally { + Glide.with(context).clear(target) + } + } } private data class LastDecoderRef( diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt similarity index 99% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt index 5d9f83388..e631af9eb 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channel.calls +package deckers.thibault.aves.channel.calls.fetchers import android.content.ContentUris import android.content.Context diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/TiffRegionFetcher.kt similarity index 95% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/TiffRegionFetcher.kt index 2d17f62d6..502553422 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/TiffRegionFetcher.kt @@ -1,4 +1,4 @@ -package deckers.thibault.aves.channel.calls +package deckers.thibault.aves.channel.calls.fetchers import android.content.Context import android.graphics.Rect @@ -13,9 +13,9 @@ class TiffRegionFetcher internal constructor( ) { fun fetch( uri: Uri, + page: Int, sampleSize: Int, regionRect: Rect, - page: Int = 0, result: MethodChannel.Result, ) { try { diff --git a/lib/model/entry.dart b/lib/model/entry.dart index ab7101131..d2244db43 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -227,8 +227,7 @@ class AvesEntry { MimeTypes.rw2, MimeTypes.srw, ].contains(mimeType) && - !isAnimated && - pageId == null; + !isAnimated; bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; From c7fcb5bc53471973b25224662520e64e8f94b4b6 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 26 Jan 2021 18:31:42 +0900 Subject: [PATCH 25/44] #39 listen to media store changes --- .../deckers/thibault/aves/MainActivity.kt | 140 ++++++++++-------- .../channel/calls/fetchers/RegionFetcher.kt | 5 - .../streams/ContentChangeStreamHandler.kt | 61 ++++++++ .../channel/streams/IntentStreamHandler.kt | 4 + lib/main.dart | 91 +++++++----- lib/model/entry.dart | 28 ++-- lib/model/source/album.dart | 2 +- lib/model/source/collection_lens.dart | 23 ++- lib/model/source/collection_source.dart | 1 + lib/model/source/media_store_source.dart | 48 +++++- lib/theme/durations.dart | 1 + lib/widgets/collection/grid/thumbnail.dart | 8 +- lib/widgets/viewer/debug_page.dart | 2 +- lib/widgets/viewer/entry_action_delegate.dart | 15 +- lib/widgets/viewer/entry_viewer_page.dart | 15 +- lib/widgets/viewer/entry_viewer_stack.dart | 26 +++- lib/widgets/viewer/info/notifications.dart | 6 + lib/widgets/viewer/printer.dart | 3 +- 18 files changed, 318 insertions(+), 161 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index 6ee05d8d1..a6742b22c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -16,24 +16,18 @@ import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.PermissionManager import io.flutter.embedding.android.FlutterActivity import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { - companion object { - private val LOG_TAG = LogUtils.createTag(MainActivity::class.java) - const val INTENT_CHANNEL = "deckers.thibault/aves/intent" - const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer" - } - - private val intentStreamHandler = IntentStreamHandler() + private lateinit var contentStreamHandler: ContentChangeStreamHandler + private lateinit var intentStreamHandler: IntentStreamHandler private lateinit var intentDataMap: MutableMap override fun onCreate(savedInstanceState: Bundle?) { Log.i(LOG_TAG, "onCreate intent=$intent") super.onCreate(savedInstanceState) - intentDataMap = extractIntentData(intent) - val messenger = flutterEngine!!.dartExecutor.binaryMessenger MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this)) @@ -48,59 +42,34 @@ class MainActivity : FlutterActivity() { StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) } StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) } + // Media Store change monitoring + contentStreamHandler = ContentChangeStreamHandler(this).apply { + EventChannel(messenger, ContentChangeStreamHandler.CHANNEL).setStreamHandler(this) + } + + // intent handling + intentStreamHandler = IntentStreamHandler().apply { + EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this) + } + intentDataMap = extractIntentData(intent) MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "getIntentData" -> { result.success(intentDataMap) intentDataMap.clear() } - "pick" -> { - val pickedUri = call.argument("uri") - if (pickedUri != null) { - val intent = Intent().apply { - data = Uri.parse(pickedUri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - setResult(RESULT_OK, intent) - } else { - setResult(RESULT_CANCELED) - } - finish() - } - + "pick" -> pick(call) } } - EventChannel(messenger, INTENT_CHANNEL).setStreamHandler(intentStreamHandler) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { setupShortcuts() } } - @RequiresApi(Build.VERSION_CODES.N_MR1) - private fun setupShortcuts() { - // do not use 'route' as extra key, as the Flutter framework acts on it - - val search = ShortcutInfoCompat.Builder(this, "search") - .setShortLabel(getString(R.string.search_shortcut_short_label)) - .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search)) - .setIntent( - Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) - .putExtra("page", "/search") - ) - .build() - - val videos = ShortcutInfoCompat.Builder(this, "videos") - .setShortLabel(getString(R.string.videos_shortcut_short_label)) - .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie)) - .setIntent( - Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) - .putExtra("page", "/collection") - .putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}")) - ) - .build() - - ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search)) + override fun onDestroy() { + contentStreamHandler.dispose() + super.onDestroy() } override fun onNewIntent(intent: Intent) { @@ -109,6 +78,25 @@ class MainActivity : FlutterActivity() { intentStreamHandler.notifyNewIntent(extractIntentData(intent)) } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) { + val treeUri = data?.data + if (resultCode != RESULT_OK || treeUri == null) { + PermissionManager.onPermissionResult(requestCode, null) + return + } + + // save access permissions across reboots + val takeFlags = (data.flags + and (Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) + contentResolver.takePersistableUriPermission(treeUri, takeFlags) + + // resume pending action + PermissionManager.onPermissionResult(requestCode, treeUri) + } + } + private fun extractIntentData(intent: Intent?): MutableMap { when (intent?.action) { Intent.ACTION_MAIN -> { @@ -138,22 +126,48 @@ class MainActivity : FlutterActivity() { return HashMap() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) { - val treeUri = data?.data - if (resultCode != RESULT_OK || treeUri == null) { - PermissionManager.onPermissionResult(requestCode, null) - return + private fun pick(call: MethodCall) { + val pickedUri = call.argument("uri") + if (pickedUri != null) { + val intent = Intent().apply { + data = Uri.parse(pickedUri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - - // save access permissions across reboots - val takeFlags = (data.flags - and (Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) - contentResolver.takePersistableUriPermission(treeUri, takeFlags) - - // resume pending action - PermissionManager.onPermissionResult(requestCode, treeUri) + setResult(RESULT_OK, intent) + } else { + setResult(RESULT_CANCELED) } + finish() + } + + @RequiresApi(Build.VERSION_CODES.N_MR1) + private fun setupShortcuts() { + // do not use 'route' as extra key, as the Flutter framework acts on it + + val search = ShortcutInfoCompat.Builder(this, "search") + .setShortLabel(getString(R.string.search_shortcut_short_label)) + .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search)) + .setIntent( + Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) + .putExtra("page", "/search") + ) + .build() + + val videos = ShortcutInfoCompat.Builder(this, "videos") + .setShortLabel(getString(R.string.videos_shortcut_short_label)) + .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie)) + .setIntent( + Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) + .putExtra("page", "/collection") + .putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}")) + ) + .build() + + ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search)) + } + + companion object { + private val LOG_TAG = LogUtils.createTag(MainActivity::class.java) + const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt index e432fac28..9b168e565 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt @@ -111,11 +111,6 @@ class RegionFetcher internal constructor( .submit() try { val bitmap = target.get() -// if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { -// bitmap = BitmapUtils.applyExifOrientation(context, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) -// } - bitmap ?: throw Exception("failed to get image from uri=$sourceUri page=$pageId") - val tempFile = File.createTempFile("aves", null, context.cacheDir).apply { deleteOnExit() outputStream().use { outputStream -> diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt new file mode 100644 index 000000000..8142c1bb9 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt @@ -0,0 +1,61 @@ +package deckers.thibault.aves.channel.streams + +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.util.Log +import deckers.thibault.aves.utils.LogUtils +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink + +class ContentChangeStreamHandler(private val context: Context) : EventChannel.StreamHandler { + private val contentObserver = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + this.onChange(selfChange, null) + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + // warning: querying the content resolver right after a change + // sometimes yields obsolete results + success(uri?.toString()) + } + } + private lateinit var eventSink: EventSink + private lateinit var handler: Handler + + init { + context.contentResolver.apply { + registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver) + registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver) + } + } + + override fun onListen(arguments: Any?, eventSink: EventSink) { + this.eventSink = eventSink + handler = Handler(Looper.getMainLooper()) + } + + override fun onCancel(arguments: Any?) {} + + fun dispose() { + context.contentResolver.unregisterContentObserver(contentObserver) + } + + private fun success(uri: String?) { + handler.post { + try { + eventSink.success(uri) + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to use event sink", e) + } + } + } + + companion object { + private val LOG_TAG = LogUtils.createTag(ContentChangeStreamHandler::class.java) + const val CHANNEL = "deckers.thibault/aves/contentchange" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt index abd594c58..c5861f208 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt @@ -18,4 +18,8 @@ class IntentStreamHandler : EventChannel.StreamHandler { fun notifyNewIntent(intentData: MutableMap?) { eventSink?.success(intentData) } + + companion object { + const val CHANNEL = "deckers.thibault/aves/intent" + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 32927e63b..02cb28210 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,9 @@ import 'dart:ui'; 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/theme/durations.dart'; import 'package:aves/theme/icons.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/home_page.dart'; @@ -45,10 +47,14 @@ class AvesApp extends StatefulWidget { class _AvesAppState extends State { Future _appSetup; + final _mediaStoreSource = MediaStoreSource(); + final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); + final List 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'); @@ -96,53 +102,18 @@ class _AvesAppState extends State { void initState() { super.initState(); _appSetup = _setup(); + _contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String)); _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map)); } - 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 (AvesApp.mode == 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), - )); - } - @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: Provider( - create: (context) => MediaStoreSource(), + child: Provider.value( + value: _mediaStoreSource, child: OverlaySupport( child: FutureBuilder( future: _appSetup, @@ -181,4 +152,48 @@ class _AvesAppState extends State { ), ); } + + 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 (AvesApp.mode == 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) { + changedUris.add(uri); + _contentChangeDebouncer(() { + _mediaStoreSource.refreshUris(List.of(changedUris)); + changedUris.clear(); + }); + } } diff --git a/lib/model/entry.dart b/lib/model/entry.dart index d2244db43..384b29a4b 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -268,12 +268,13 @@ class AvesEntry { } } - // The additional comparison of width to height is a workaround for badly registered entries. - // e.g. a portrait FHD video should be registered as width=1920, height=1080, orientation=90, - // but is incorrectly registered in the Media Store as width=1080, height=1920, orientation=0 - // Double-checking the width/height during loading or cataloguing is the proper solution, - // but it would take space and time, so a basic workaround will do. - bool get isPortrait => rotationDegrees % 180 == 90 && (catalogMetadata?.rotationDegrees == null || width > height); + // Media Store size/rotation is inaccurate, e.g. a portrait FHD video is rotated according to its metadata, + // so it should be registered as width=1920, height=1080, orientation=90, + // but is incorrectly registered as width=1080, height=1920, orientation=0. + // Double-checking the width/height during loading or cataloguing is the proper solution, but it would take space and time. + // Comparing width and height can help with the portrait FHD video example, + // but it fails for a portrait screenshot rotated, which is landscape with width=1080, height=1920, orientation=90 + bool get isRotated => rotationDegrees % 180 == 90; static const ratioSeparator = '\u2236'; static const resolutionSeparator = ' \u00D7 '; @@ -281,7 +282,7 @@ class AvesEntry { String get resolutionText { final ws = width ?? '?'; final hs = height ?? '?'; - return isPortrait ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs'; + return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs'; } String get aspectRatioText { @@ -289,7 +290,7 @@ class AvesEntry { final gcd = width.gcd(height); final w = width ~/ gcd; final h = height ~/ gcd; - return isPortrait ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h'; + return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h'; } else { return '?$ratioSeparator?'; } @@ -297,13 +298,13 @@ class AvesEntry { double get displayAspectRatio { if (width == 0 || height == 0) return 1; - return isPortrait ? height / width : width / height; + return isRotated ? height / width : width / height; } Size get displaySize { final w = width.toDouble(); final h = height.toDouble(); - return isPortrait ? Size(h, w) : Size(w, h); + return isRotated ? Size(h, w) : Size(w, h); } int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null; @@ -636,7 +637,10 @@ class AvesEntry { // 1) date descending // 2) name descending static int compareByDate(AvesEntry a, AvesEntry b) { - final c = (b.bestDate ?? _epoch).compareTo(a.bestDate ?? _epoch); - return c != 0 ? c : -compareByName(a, b); + var c = (b.bestDate ?? _epoch).compareTo(a.bestDate ?? _epoch); + if (c != 0) return c; + c = (b.dateModifiedSecs ?? 0).compareTo(a.dateModifiedSecs ?? 0); + if (c != 0) return c; + return -compareByName(a, b); } } diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index b50aa3777..44b2789c1 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -53,7 +53,7 @@ mixin AlbumMixin on SourceBase { Map getAlbumEntries() { final entries = sortedEntriesForFilterList; final regularAlbums = [], appAlbums = [], specialAlbums = []; - for (var album in sortedAlbums) { + for (final album in sortedAlbums) { switch (androidFileUtils.getAlbumType(album)) { case AlbumType.regular: regularAlbums.add(album); diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index de6e12bf7..a151de471 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -19,6 +19,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel EntryGroupFactor groupFactor; EntrySortFactor sortFactor; final AChangeNotifier filterChangeNotifier = AChangeNotifier(); + bool listenToSource; List _filteredEntries; List _subscriptions = []; @@ -30,13 +31,16 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel Iterable filters, @required EntryGroupFactor groupFactor, @required EntrySortFactor sortFactor, + this.listenToSource = true, }) : filters = {if (filters != null) ...filters.where((f) => f != null)}, groupFactor = groupFactor ?? EntryGroupFactor.month, sortFactor = sortFactor ?? EntrySortFactor.date { - _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); - _subscriptions.add(source.eventBus.on().listen((e) => onEntryRemoved(e.entries))); - _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); - _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + if (listenToSource) { + _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + _subscriptions.add(source.eventBus.on().listen((e) => onEntryRemoved(e.entries))); + _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + _subscriptions.add(source.eventBus.on().listen((e) => _refresh())); + } _refresh(); } @@ -49,15 +53,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel super.dispose(); } - CollectionLens derive(CollectionFilter filter) { - return CollectionLens( - source: source, - filters: filters, - groupFactor: groupFactor, - sortFactor: sortFactor, - )..addFilter(filter); - } - bool get isEmpty => _filteredEntries.isEmpty; int get entryCount => _filteredEntries.length; @@ -82,7 +77,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel return true; } - Object heroTag(AvesEntry entry) => '$hashCode${entry.uri}'; + Object heroTag(AvesEntry entry) => entry.uri; void addFilter(CollectionFilter filter) { if (filter == null || filters.contains(filter)) return; diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 37140c22a..137ca9786 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -56,6 +56,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } void addAll(Iterable entries) { + if (entries.isEmpty) return; if (_rawEntries.isNotEmpty) { final newContentIds = entries.map((entry) => entry.contentId).toList(); _rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId)); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index f21ab63ab..a52e78b80 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:aves/model/entry.dart'; @@ -40,6 +41,7 @@ class MediaStoreSource extends CollectionSource { @override Future refresh() async { + assert(_initialized); debugPrint('$runtimeType refresh start'); final stopwatch = Stopwatch()..start(); stateNotifier.value = SourceState.loading; @@ -47,8 +49,8 @@ class MediaStoreSource extends CollectionSource { final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries final knownEntryMap = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs))); - final obsoleteEntries = (await ImageFileService.getObsoleteEntries(knownEntryMap.keys.toList())).toSet(); - oldEntries.removeWhere((entry) => obsoleteEntries.contains(entry.contentId)); + final obsoleteContentIds = (await ImageFileService.getObsoleteEntries(knownEntryMap.keys.toList())).toSet(); + oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId)); // show known entries addAll(oldEntries); @@ -57,9 +59,10 @@ class MediaStoreSource extends CollectionSource { debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}'); // clean up obsolete entries - metadataDb.removeIds(obsoleteEntries, updateFavourites: true); + metadataDb.removeIds(obsoleteContentIds, updateFavourites: true); // fetch new entries + // refresh after the first 10 entries, then after 100 more, then every 1000 entries var refreshCount = 10; const refreshCountMax = 1000; final allNewEntries = [], pendingNewEntries = []; @@ -102,6 +105,45 @@ class MediaStoreSource extends CollectionSource { ); } + Future refreshUris(List changedUris) async { + assert(_initialized); + debugPrint('$runtimeType refreshUris uris=$changedUris'); + + final uriByContentId = Map.fromEntries(changedUris.map((uri) { + if (uri == null) return null; + final idString = Uri.parse(uri).pathSegments.last; + return MapEntry(int.tryParse(idString), uri); + }).where((kv) => kv != null)); + + // clean up obsolete entries + final obsoleteContentIds = (await ImageFileService.getObsoleteEntries(uriByContentId.keys.toList())).toSet(); + uriByContentId.removeWhere((contentId, _) => obsoleteContentIds.contains(contentId)); + metadataDb.removeIds(obsoleteContentIds, updateFavourites: true); + + // add new entries + final newEntries = []; + for (final kv in uriByContentId.entries) { + final contentId = kv.key; + final uri = kv.value; + final sourceEntry = await ImageFileService.getEntry(uri, null); + final existingEntry = rawEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); + if (existingEntry == null || sourceEntry.dateModifiedSecs > existingEntry.dateModifiedSecs) { + newEntries.add(sourceEntry); + } + } + addAll(newEntries); + await metadataDb.saveEntries(newEntries); + updateAlbums(); + + stateNotifier.value = SourceState.cataloguing; + await catalogEntries(); + + stateNotifier.value = SourceState.locating; + await locateEntries(); + + stateNotifier.value = SourceState.ready; + } + @override Future refreshMetadata(Set entries) { final contentIds = entries.map((entry) => entry.contentId).toSet(); diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 59a0f2a64..51fa3a18f 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -48,4 +48,5 @@ class Durations { static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const softKeyboardDisplayDelay = Duration(milliseconds: 300); static const searchDebounceDelay = Duration(milliseconds: 250); + static const contentChangeDebounceDelay = Duration(milliseconds: 1000); } diff --git a/lib/widgets/collection/grid/thumbnail.dart b/lib/widgets/collection/grid/thumbnail.dart index 23d31c827..3eabb767c 100644 --- a/lib/widgets/collection/grid/thumbnail.dart +++ b/lib/widgets/collection/grid/thumbnail.dart @@ -55,7 +55,13 @@ class InteractiveThumbnail extends StatelessWidget { TransparentMaterialPageRoute( settings: RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (c, a, sa) => EntryViewerPage( - collection: collection, + collection: CollectionLens( + source: collection.source, + filters: collection.filters, + groupFactor: collection.groupFactor, + sortFactor: collection.sortFactor, + listenToSource: false, + ), initialEntry: entry, ), ), diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index d26324622..bd1f7ce21 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -77,8 +77,8 @@ class ViewerDebugPage extends StatelessWidget { 'height': '${entry.height}', 'sourceRotationDegrees': '${entry.sourceRotationDegrees}', 'rotationDegrees': '${entry.rotationDegrees}', + 'isRotated': '${entry.isRotated}', 'isFlipped': '${entry.isFlipped}', - 'portrait': '${entry.isPortrait}', 'displayAspectRatio': '${entry.displayAspectRatio}', 'displaySize': '${entry.displaySize}', }), diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index d81211a9e..d201c698f 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -16,11 +16,11 @@ 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/info/notifications.dart'; import 'package:aves/widgets/viewer/printer.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:pedantic/pedantic.dart'; import 'package:provider/provider.dart'; @@ -139,15 +139,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await entry.delete()) { showFeedback(context, 'Failed'); - } else if (hasCollection) { - // update collection - collection.source.removeEntries([entry]); - if (collection.sortedEntries.isEmpty) { - Navigator.pop(context); - } } else { - // leave viewer - unawaited(SystemNavigator.pop()); + if (hasCollection) { + collection.source.removeEntries([entry]); + } + EntryDeletedNotification(entry).dispatch(context); } } @@ -199,7 +195,6 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } else { showFeedback(context, 'Done!'); } - source.refresh(); }, ); } diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 8bb27913c..f285d8292 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -20,17 +20,10 @@ class EntryViewerPage extends StatelessWidget { Widget build(BuildContext context) { return MediaQueryDataProvider( child: Scaffold( - body: collection != null - ? AnimatedBuilder( - animation: collection, - builder: (context, child) => EntryViewerStack( - collection: collection, - initialEntry: initialEntry, - ), - ) - : EntryViewerStack( - initialEntry: initialEntry, - ), + body: EntryViewerStack( + collection: collection, + initialEntry: initialEntry, + ), backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black, resizeToAvoidBottomInset: false, ), diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index b9867800f..7ede8f873 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -164,6 +164,8 @@ class _EntryViewerStackState extends State with SingleTickerPr _goToCollection(notification.filter); } else if (notification is ViewStateNotification) { _updateViewState(notification.uri, notification.viewState); + } else if (notification is EntryDeletedNotification) { + _onEntryDeleted(context, notification.entry); } return false; }, @@ -324,7 +326,14 @@ class _EntryViewerStackState extends State with SingleTickerPr context, MaterialPageRoute( settings: RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage(collection.derive(filter)), + builder: (context) => CollectionPage( + CollectionLens( + source: collection.source, + filters: collection.filters, + groupFactor: collection.groupFactor, + sortFactor: collection.sortFactor, + )..addFilter(filter), + ), ), (route) => false, ); @@ -356,6 +365,21 @@ class _EntryViewerStackState extends State with SingleTickerPr _updateEntry(); } + void _onEntryDeleted(BuildContext context, AvesEntry entry) { + if (hasCollection) { + final entries = collection.sortedEntries; + entries.remove(entry); + if (entries.isEmpty) { + Navigator.pop(context); + } else { + _onCollectionChange(); + } + } else { + // leave viewer + SystemNavigator.pop(); + } + } + void _updateEntry() { if (_currentHorizontalPage != null && entries.isNotEmpty && _currentHorizontalPage >= entries.length) { // as of Flutter v1.22.2, `PageView` does not call `onPageChanged` when the last page is deleted diff --git a/lib/widgets/viewer/info/notifications.dart b/lib/widgets/viewer/info/notifications.dart index 32afe7ae9..ed2da66ce 100644 --- a/lib/widgets/viewer/info/notifications.dart +++ b/lib/widgets/viewer/info/notifications.dart @@ -11,6 +11,12 @@ class FilterNotification extends Notification { const FilterNotification(this.filter); } +class EntryDeletedNotification extends Notification { + final AvesEntry entry; + + const EntryDeletedNotification(this.entry); +} + class OpenTempEntryNotification extends Notification { final AvesEntry entry; diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index bab2dc5a7..d774f5d38 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -32,8 +32,9 @@ class EntryPrinter { void _addPdfPage(pdf.Widget pdfChild) { if (pdfChild == null) return; + final displaySize = entry.displaySize; pages.add(pdf.Page( - orientation: entry.isPortrait ? pdf.PageOrientation.portrait : pdf.PageOrientation.landscape, + orientation: displaySize.height > displaySize.width ? pdf.PageOrientation.portrait : pdf.PageOrientation.landscape, build: (context) => pdf.FullPage( ignoreMargins: true, child: pdf.Center( From 79b627684661033a70cc7cd3ef48c23c401eb99d Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Jan 2021 10:07:21 +0900 Subject: [PATCH 26/44] fixed media store monitoring delay to avoid temp entries --- lib/theme/durations.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 51fa3a18f..9cc883fda 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -48,5 +48,11 @@ class Durations { static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const softKeyboardDisplayDelay = Duration(milliseconds: 300); static const searchDebounceDelay = Duration(milliseconds: 250); - static const contentChangeDebounceDelay = Duration(milliseconds: 1000); + + // Content change monitoring delay should be large enough, + // so that querying the Media Store yields final entries. + // For example, when taking a picture with a Galaxy S10e default camera app, + // querying the Media Store just 1 second after sometimes yields an entry with + // its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg` + static const contentChangeDebounceDelay = Duration(milliseconds: 1500); } From 79aefc3aa5781644a9bae80efc75a4dac9b61147 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Jan 2021 11:03:55 +0900 Subject: [PATCH 27/44] panorama: loosened identification criteria, handle missing parameter --- .../aves/channel/calls/MetadataHandler.kt | 16 ++++++------ .../deckers/thibault/aves/metadata/XMP.kt | 7 +++--- lib/model/panorama.dart | 25 +++++++++++++++---- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 2a5355ba8..689d79458 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -594,14 +594,16 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { val metadata = ImageMetadataReader.readMetadata(input) val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java) try { - fun getProp(propName: String): Int? = xmpDirs.map { it.xmpMeta.getPropertyInteger(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } + fun getIntProp(propName: String): Int? = xmpDirs.map { it.xmpMeta.getPropertyInteger(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } + fun getStringProp(propName: String): String? = xmpDirs.map { it.xmpMeta.getPropertyString(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null } val fields: FieldMap = hashMapOf( - "croppedAreaLeft" to getProp(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME), - "croppedAreaTop" to getProp(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME), - "croppedAreaWidth" to getProp(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME), - "croppedAreaHeight" to getProp(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME), - "fullPanoWidth" to getProp(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME), - "fullPanoHeight" to getProp(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME), + "croppedAreaLeft" to getIntProp(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME), + "croppedAreaTop" to getIntProp(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME), + "croppedAreaWidth" to getIntProp(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME), + "croppedAreaHeight" to getIntProp(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME), + "fullPanoWidth" to getIntProp(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME), + "fullPanoHeight" to getIntProp(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME), + "projectionType" to (getStringProp(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT), ) result.success(fields) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index bd7ff1733..0ca46696b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -54,18 +54,19 @@ object XMP { const val GPANO_CROPPED_AREA_TOP_PROP_NAME = "GPano:CroppedAreaTopPixels" const val GPANO_FULL_PANO_HEIGHT_PROP_NAME = "GPano:FullPanoHeightPixels" const val GPANO_FULL_PANO_WIDTH_PROP_NAME = "GPano:FullPanoWidthPixels" - private const val GPANO_PROJECTION_TYPE_PROP_NAME = "GPano:ProjectionType" + const val GPANO_PROJECTION_TYPE_PROP_NAME = "GPano:ProjectionType" + const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular" private const val PMTM_IS_PANO360 = "pmtm:IsPano360" + // `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default + // `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode) private val gpanoRequiredProps = listOf( GPANO_CROPPED_AREA_HEIGHT_PROP_NAME, GPANO_CROPPED_AREA_WIDTH_PROP_NAME, GPANO_CROPPED_AREA_LEFT_PROP_NAME, GPANO_CROPPED_AREA_TOP_PROP_NAME, - GPANO_FULL_PANO_HEIGHT_PROP_NAME, GPANO_FULL_PANO_WIDTH_PROP_NAME, - GPANO_PROJECTION_TYPE_PROP_NAME, ) // extensions diff --git a/lib/model/panorama.dart b/lib/model/panorama.dart index 0bebe9501..99d1ff318 100644 --- a/lib/model/panorama.dart +++ b/lib/model/panorama.dart @@ -4,24 +4,38 @@ import 'package:flutter/widgets.dart'; class PanoramaInfo { final Rect croppedAreaRect; final Size fullPanoSize; + final String projectionType; PanoramaInfo({ this.croppedAreaRect, this.fullPanoSize, + this.projectionType, }); factory PanoramaInfo.fromMap(Map map) { - final cLeft = map['croppedAreaLeft'] as int; - final cTop = map['croppedAreaTop'] as int; + 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) { + // assume the cropped area is actually covering 360 degrees horizontally + // even when `croppedAreaLeft` is non zero + fWidth = cWidth; + fHeight = (fWidth / 2).round(); + cTop = ((fHeight - cHeight) / 2).round(); + cLeft = 0; + } + Rect croppedAreaRect; if (cLeft != null && cTop != null && cWidth != null && cHeight != null) { croppedAreaRect = Rect.fromLTWH(cLeft.toDouble(), cTop.toDouble(), cWidth.toDouble(), cHeight.toDouble()); } - final fWidth = map['fullPanoWidth'] as int; - final fHeight = map['fullPanoHeight'] as int; Size fullPanoSize; if (fWidth != null && fHeight != null) { fullPanoSize = Size(fWidth.toDouble(), fHeight.toDouble()); @@ -30,11 +44,12 @@ class PanoramaInfo { return PanoramaInfo( croppedAreaRect: croppedAreaRect, fullPanoSize: fullPanoSize, + projectionType: projectionType, ); } bool get hasCroppedArea => croppedAreaRect != null && fullPanoSize != null; @override - String toString() => '$runtimeType#${shortHash(this)}{croppedAreaRect=$croppedAreaRect, fullPanoSize=$fullPanoSize}'; + String toString() => '$runtimeType#${shortHash(this)}{croppedAreaRect=$croppedAreaRect, fullPanoSize=$fullPanoSize, projectionType=$projectionType}'; } From 797f8a8d0753d03fae2d9c5b73dd1cc5994c37a1 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Jan 2021 12:43:14 +0900 Subject: [PATCH 28/44] improved file op report overlay --- .../common/action_mixins/feedback.dart | 111 +++++++++++++----- 1 file changed, 83 insertions(+), 28 deletions(-) diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 66156eae3..3a0120985 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -35,11 +35,60 @@ mixin FeedbackMixin { @required Stream opStream, @required void Function(Set processed) onDone, }) { - final processed = {}; + _opReportOverlayEntry = OverlayEntry( + builder: (context) => ReportOverlay( + opStream: opStream, + itemCount: selection.length, + onDone: (processed) { + _opReportOverlayEntry?.remove(); + _opReportOverlayEntry = null; + onDone(processed); + }, + ), + ); + Overlay.of(context).insert(_opReportOverlayEntry); + } +} + +class ReportOverlay extends StatefulWidget { + final Stream opStream; + final int itemCount; + final void Function(Set processed) onDone; + + const ReportOverlay({ + @required this.opStream, + @required this.itemCount, + @required this.onDone, + }); + + @override + _ReportOverlayState createState() => _ReportOverlayState(); +} + +class _ReportOverlayState extends State> with SingleTickerProviderStateMixin { + final processed = {}; + AnimationController _animationController; + Animation _animation; + + Stream get opStream => widget.opStream; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + duration: Durations.collectionOpOverlayAnimation, + vsync: this, + ); + _animation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutQuad, + ); + _animationController.forward(); // do not handle completion inside `StreamBuilder` // as it could be called multiple times - Future onComplete() => _hideOpReportOverlay().then((_) => onDone(processed)); + Future onComplete() => _animationController.reverse().then((_) => widget.onDone(processed)); opStream.listen( processed.add, onError: (error) { @@ -48,17 +97,34 @@ mixin FeedbackMixin { }, onDone: onComplete, ); + } - _opReportOverlayEntry = OverlayEntry( - builder: (context) { - return AbsorbPointer( - child: StreamBuilder( - stream: opStream, - builder: (context, snapshot) { - Widget child = SizedBox.shrink(); - if (!snapshot.hasError) { - final percent = processed.length.toDouble() / selection.length; - child = CircularPercentIndicator( + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AbsorbPointer( + child: StreamBuilder( + stream: opStream, + builder: (context, snapshot) { + final percent = processed.length.toDouble() / widget.itemCount; + return FadeTransition( + opacity: _animation, + child: Container( + decoration: BoxDecoration( + gradient: RadialGradient( + colors: [ + Colors.black, + Colors.black54, + ], + ), + ), + child: Center( + child: CircularPercentIndicator( percent: percent, lineWidth: 16, radius: 160, @@ -67,22 +133,11 @@ mixin FeedbackMixin { animation: true, center: Text(NumberFormat.percentPattern().format(percent)), animateFromLastPercent: true, - ); - } - return AnimatedSwitcher( - duration: Durations.collectionOpOverlayAnimation, - child: child, - ); - }), - ); - }, + ), + ), + ), + ); + }), ); - Overlay.of(context).insert(_opReportOverlayEntry); - } - - Future _hideOpReportOverlay() async { - await Future.delayed(Durations.collectionOpOverlayAnimation * timeDilation); - _opReportOverlayEntry?.remove(); - _opReportOverlayEntry = null; } } From 473db9174d8639cf357914697d8c2762c01fe3b0 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Jan 2021 17:46:34 +0900 Subject: [PATCH 29/44] upgrade flutter to stable v1.22.6 + packages upgrade --- .github/workflows/check.yml | 2 +- .github/workflows/release.yml | 6 +++--- CHANGELOG.md | 2 ++ pubspec.lock | 8 ++++---- shaders_1.22.5.sksl.json | 1 - shaders_1.22.6.sksl.json | 1 + 6 files changed, 11 insertions(+), 9 deletions(-) delete mode 100644 shaders_1.22.5.sksl.json create mode 100644 shaders_1.22.6.sksl.json diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3b4fc72b1..555bbde2e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,7 +15,7 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: stable - flutter-version: '1.22.5' + flutter-version: '1.22.6' - name: Clone the repository. uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4bd20b5c..d35450f7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: stable - flutter-version: '1.22.5' + flutter-version: '1.22.6' # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): # https://issuetracker.google.com/issues/144111441 @@ -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_1.22.5.sksl.json - flutter build appbundle --bundle-sksl-path shaders_1.22.5.sksl.json + flutter build apk --bundle-sksl-path shaders_1.22.6.sksl.json + flutter build appbundle --bundle-sksl-path shaders_1.22.6.sksl.json rm $AVES_STORE_FILE env: AVES_STORE_FILE: ${{ github.workspace }}/key.jks diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a089b539..ec203e5be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Changed +- Upgraded Flutter to stable v1.22.6 ## [v1.3.2] - 2021-01-17 ### Added diff --git a/pubspec.lock b/pubspec.lock index edd985531..b3b97f713 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -554,7 +554,7 @@ packages: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "1.4.12" + version: "1.4.13" octo_image: dependency: transitive description: @@ -750,7 +750,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.3.2+4" + version: "4.3.3" pub_semver: dependency: transitive description: @@ -827,7 +827,7 @@ packages: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+3" + version: "0.0.2+2" shelf: dependency: transitive description: @@ -895,7 +895,7 @@ packages: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.3+1" stack_trace: dependency: transitive description: diff --git a/shaders_1.22.5.sksl.json b/shaders_1.22.5.sksl.json deleted file mode 100644 index a7927453a..000000000 --- a/shaders_1.22.5.sksl.json +++ /dev/null @@ -1 +0,0 @@ -{"platform":"android","name":"SM G970N","engineRevision":"ae90085a8437c0ae94d6b5ad2741739ebc742cb4","data":{"CAZAAAICBIAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEANwQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IG91dHB1dENvdmVyYWdlX1N0YWdlMCAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABMAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAECA4AAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAWwEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAoAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAICCEAABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAABQAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAAAAAAAAAYAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6AAAAAAABQAAAALQAEAAAEAAAAAAAAAAAACAAAABWAAFYAAQAAAAAAAAAAAAQAAAAHYACYAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAEBAAAAAAAAAQEA3w8AAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVyZWN0VW5pZm9ybV9TdGFnZTI7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCWhhbGYgYWxwaGEgPSAxLjA7CgkJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UyOwoJewoJCS8vIFN0YWdlIDIsIEFBUmVjdEVmZmVjdAoJCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJCWhhbGYgYWxwaGE7CgkJQHN3aXRjaCAoMSkgCgkJewoJCQljYXNlIDA6ICAgIGNhc2UgMjogICAgICAgIGFscGhhID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1N0YWdlMi56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fU3RhZ2UyLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7CgkJCWJyZWFrOwoJCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCQl4U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnggLSB1cmVjdFVuaWZvcm1fU3RhZ2UyLngpLCAwLjApOwoJCQl4U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTIueiAtIHNrX0ZyYWdDb29yZC54KSwgMC4wKTsKCQkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMi55KSwgMC4wKTsKCQkJeVN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UyLncgLSBza19GcmFnQ29vcmQueSksIDAuMCk7CgkJCWFscGhhID0gKDEuMCArIG1heCh4U3ViLCAtMS4wKSkgKiAoMS4wICsgbWF4KHlTdWIsIC0xLjApKTsKCQl9CgkJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCQl7CgkJCWFscGhhID0gMS4wIC0gYWxwaGE7CgkJfQoJCWhhbGY0IGlucHV0Q29sb3IgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0X1N0YWdlMiA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0X1N0YWdlMjsKCX0KfQoAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZACAICCEAABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAABQAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAABAAAAAAYAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6ACAAAAABQAAAALQAEAAAEAAAAAAAAAAAACAAAABWAAFYAAQAAAAAAAAAAAAQAAAAHYACYAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAEBAAAAAAAAAQEA3g8AAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVyZWN0VW5pZm9ybV9TdGFnZTI7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBhbHBoYSA9IDEuMDsKCQlhbHBoYSA9IHZpbkNvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTI7Cgl7CgkJLy8gU3RhZ2UgMiwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UyLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTIueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTIueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMi56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UyLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTIudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UyID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRfU3RhZ2UyOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAICBIAAAAIAAAABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1M6CQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAdQQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJCWZsb2F0IGZud2lkdGggPSBhYnMoZ3gpICsgYWJzKGd5KTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAECBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABAAAAAABAAAQAABBAMADYAB4AACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1NdAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAACcEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwLnksIHVjbGFtcF9TdGFnZTFfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJX291dHB1dCA9IHRleHR1cmVDb2xvcjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE1hdHJpeEVmZmVjdAoJCW91dHB1dF9TdGFnZTEgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAACBAAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAArQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECAUAAAEYAAEABYAARAANQAAQAAAAAAAAMABEQAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1OFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFJSZWN0U2hhZG93Cgl2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAB1AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUlJlY3RTaGFkb3cKCQloYWxmMyBzaGFkb3dQYXJhbXM7CgkJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CgkJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJCWhhbGYgZmFjdG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdXYpLnJycnIuYTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChmYWN0b3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","CAZAAAECAYAAABAAAAAACAAAAAJQAAIA777777YPAAKAAABBAMABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1P9AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoaW5Qb3NpdGlvbi54ICwgaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABLAgAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZjQgdGV4Q29sb3I7CgkJewoJCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHZUZXh0dXJlQ29vcmRzX1N0YWdlMCk7CgkJfQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IG91dHB1dENvbG9yX1N0YWdlMCAqIHRleENvbG9yOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACAYAAAAAIAAABGAABAD7777777777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1PkAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAWgEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAEAAAAKAAAAaW5Qb3NpdGlvbgAAAQAAAAAAAAA=","CAZAAAMCBEAAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAEBAAAAAAAAAQEA5QIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAAAAAIIDAAWAATYABEAAAAABAAAAAABBAMADYAB4AACQAAAABQAAAAABAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAABKAADAAKQAAYACUAAGAATAAAQAFIAAMABKAADAAOAAEIAEAADEAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MXBAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgcmFkaWk7CglyYWRpaS54ID0gZG90KHJhZGlpX3NlbGVjdG9yLCByYWRpaV94KTsKCXJhZGlpLnkgPSBkb3QocmFkaWlfc2VsZWN0b3IsIHJhZGlpX3kpOwoJYm9vbCBpc19hcmNfc2VjdGlvbiA9IChyYWRpaS54ID4gMCk7CglyYWRpaSA9IGFicyhyYWRpaSk7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpOwoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZTsKCWlmIChpc19hcmNfc2VjdGlvbikgCgl7CgkJdmFyY2Nvb3JkX1N0YWdlMC54eSA9IDEgLSBhYnMocmFkaXVzX291dHNldCk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqICh2YXJjY29vcmRfU3RhZ2UwLnh5L3JhZGlpICogY29ybmVyICogMik7Cgl9CgllbHNlIAoJewoJCXZhcmNjb29yZF9TdGFnZTAgPSBmbG9hdDQoMCk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAACgCAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0NCB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgkJaWYgKGZsb2F0MigwKSAhPSB2YXJjY29vcmRfU3RhZ2UwLnh5KSAKCQl7CgkJCWZsb2F0IGZuID0gZG90KHZhcmNjb29yZF9TdGFnZTAueHksIHZhcmNjb29yZF9TdGFnZTAueHkpIC0gMTsKCQkJaWYgKGZuID4gMCkgCgkJCXsKCQkJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDApOwoJCQl9CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAHAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECAYAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAZAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACBAAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAA4AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAEAAAIIDAAWAATYABEAAAAAAAAAQAABBAMADYAB4AACQAAAABQAAAAAAAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACA4AAAAYAAAAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALQAAAAAAAAAAAAAAAQAAAACYACYAA":"AgAAAExTS1PjAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNEaW1lbnNpb25zSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSAodGV4SWR4KTsKCXZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAXAwAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBmbG9hdDIgdkludFRleHR1cmVDb29yZHNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGlzdGFuY2VGaWVsZFBhdGgKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfU3RhZ2UwOwoJCWhhbGY0IHRleENvbG9yOwoJCXsKCQkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dikucnJycjsKCQl9CgkJaGFsZiBkaXN0YW5jZSA9IDcuOTY4NzUqKHRleENvbG9yLnIgLSAwLjUwMTk2MDc4NDMxKTsKCQloYWxmIGFmd2lkdGg7CgkJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeSh2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTAueSkpKTsKCQloYWxmIHZhbCA9IHNtb290aHN0ZXAoLWFmd2lkdGgsIGFmd2lkdGgsIGRpc3RhbmNlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh2YWwpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZACAACB4AAAAAAAAAGOAIAAAJQAAIACIAAAAA4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAKAAAABWAAWAA":"AgAAAExTS1McAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBwb3NpdGlvbiA9IHBvc2l0aW9uLnh5OwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJdmNvdmVyYWdlX1N0YWdlMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAvQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfU3RhZ2UxKSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAICCAAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAAYAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAQAAAAAMAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPABAAAAAAYAAAAFIACAAACAAAAAAAAAAAABAAAAAZAAC4AAIAAAAAAAAAAAAIAAAADUABMAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAABAQAAAAAAAAEBAGMRAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UyOwppbiBoYWxmNCB2SGFpclF1YWRFZGdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAodHJ1ZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBnRiA9IGhhbGYyKDIuMCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiBkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHkueCAtIGR1dmR5LnkpOwoJCWVkZ2VBbHBoYSA9IGhhbGYodkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggLSB2SGFpclF1YWRFZGdlX1N0YWdlMC55KTsKCQllZGdlQWxwaGEgPSBzcXJ0KGVkZ2VBbHBoYSAqIGVkZ2VBbHBoYSAvIGRvdChnRiwgZ0YpKTsKCQllZGdlQWxwaGEgPSBtYXgoMS4wIC0gZWRnZUFscGhhLCAwLjApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTI7Cgl7CgkJLy8gU3RhZ2UgMiwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UyLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTIueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTIueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMi56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UyLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTIudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UyID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRfU3RhZ2UyOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABEAAAAAAAAAAAABBAMADYAB4AACQAAAABQAAAAAAAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAOxMAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgX2Nvb3Jkcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACAYAABEAJAAABGAABAAOAAEIA777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAABVAQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZAAAECA4AAAAAAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAEwCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAMCBAAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAABAAAAAKAAC4AAIAAAAAAAAAAAAIAAAABYABMAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAQEAAAAAAAABAQCCBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","CAZACAACB4AABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAABQAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAABAAAAAAYAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6ACAAAAABQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAAdQwAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBhbHBoYSA9IDEuMDsKCQlhbHBoYSA9IHZpbkNvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","CAZACAICCAAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAAYAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAAAAAAAMAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAAYAAAAFIACAAACAAAAAAAAAAAABAAAAAZAAC4AAIAAAAAAAAAAAAIAAAADUABMAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAABAQAAAAAAAAEBAGQRAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UyOwppbiBoYWxmNCB2SGFpclF1YWRFZGdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UyOwoJewoJCS8vIFN0YWdlIDIsIEFBUmVjdEVmZmVjdAoJCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJCWhhbGYgYWxwaGE7CgkJQHN3aXRjaCAoMSkgCgkJewoJCQljYXNlIDA6ICAgIGNhc2UgMjogICAgICAgIGFscGhhID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1N0YWdlMi56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fU3RhZ2UyLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7CgkJCWJyZWFrOwoJCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCQl4U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnggLSB1cmVjdFVuaWZvcm1fU3RhZ2UyLngpLCAwLjApOwoJCQl4U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTIueiAtIHNrX0ZyYWdDb29yZC54KSwgMC4wKTsKCQkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMi55KSwgMC4wKTsKCQkJeVN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UyLncgLSBza19GcmFnQ29vcmQueSksIDAuMCk7CgkJCWFscGhhID0gKDEuMCArIG1heCh4U3ViLCAtMS4wKSkgKiAoMS4wICsgbWF4KHlTdWIsIC0xLjApKTsKCQl9CgkJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCQl7CgkJCWFscGhhID0gMS4wIC0gYWxwaGE7CgkJfQoJCWhhbGY0IGlucHV0Q29sb3IgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0X1N0YWdlMiA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0X1N0YWdlMjsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZACAECBUAABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAOAASQAAUAAAABEAA4AACAAAAACYAB4AACAAAAAAAAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAUAAIAAAIAAAAAAAAAAAAIAAAADAABMAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAArgcAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMTsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7Cglfb3V0cHV0ID0gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MxLCB0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCWhhbGYgYWxwaGEgPSAxLjA7CgkJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","CAZACAECBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABAAAAAAAAAAAAABBAMADYAB4AACAAAAAAAAAAAANAAAAAAAAAAAAAIIDABKAAAQAAQAAAAAAAAAAAAQAAAAGQACYAA":"AgAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAACIAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpKSwgb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAACCAAAAAYAAEAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALQAAAAAAAAEAAAAAKAAKIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAAAAAAAACAAAAAJAACAAAEAAAAAAAAAAAAAPAAHQADYAB4AAAAAAAEAAAAAZAAIAAAIAAAAAAAAAAAAIAAAADUABMAA":"AgAAAExTS1PlAwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEaXN0YW5jZUZpZWxkUGF0aAoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzRGltZW5zaW9uc0ludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAAFoOAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNl83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM2XzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBVbnJvbGxlZEJpbmFyeUdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCWZsb2F0NCBzY2FsZSwgYmlhczsKCWlmICg0IDw9IDQgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS53KSAKCXsKCQlpZiAoNCA8PSAyIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoNCA8PSAxIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueCkgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDQgPD0gMyB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNl83X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczZfN19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICg0IDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoNCA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoNCA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdXYgPSB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CgkJaGFsZjQgdGV4Q29sb3I7CgkJewoJCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHV2KS5ycnJyOwoJCX0KCQloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJCWhhbGYgYWZ3aWR0aDsKCQlhZndpZHRoID0gYWJzKDAuNjUqaGFsZihkRmR5KHZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMC55KSkpOwoJCWhhbGYgdmFsID0gc21vb3Roc3RlcCgtYWZ3aWR0aCwgYWZ3aWR0aCwgZGlzdGFuY2UpOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KHZhbCk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABEAAAAABAAAQAABBAMADYAB4AACQAAAABQAAAAABAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAshQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gY2xhbXAoc3Vic2V0Q29vcmQueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCV9vdXRwdXQgPSB0ZXh0dXJlQ29sb3I7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZACAACB4AABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAABQAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAAAAAAAAAYAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6AAAAAAABQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAAdgwAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCWhhbGYgYWxwaGEgPSAxLjA7CgkJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","CAZAAAECAUAAAAAAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1NuAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAB+AwAAaW4gaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAACBAAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAFgIAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZACAACBYAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAAYAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAQAAAAAMAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPABAAAAAAYAAAAFIACAAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAPoNAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2SGFpclF1YWRFZGdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAodHJ1ZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBnRiA9IGhhbGYyKDIuMCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiBkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHkueCAtIGR1dmR5LnkpOwoJCWVkZ2VBbHBoYSA9IGhhbGYodkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggLSB2SGFpclF1YWRFZGdlX1N0YWdlMC55KTsKCQllZGdlQWxwaGEgPSBzcXJ0KGVkZ2VBbHBoYSAqIGVkZ2VBbHBoYSAvIGRvdChnRiwgZ0YpKTsKCQllZGdlQWxwaGEgPSBtYXgoMS4wIC0gZWRnZUFscGhhLCAwLjApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAOAAAAaW5IYWlyUXVhZEVkZ2UAAAEAAAAAAAAA","CAZAAAACAYAAAEAYAAABGAABAAOAAEIA7777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NnAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBjb2xvciA9IGluQ29sb3I7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAVQEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAECA4AAAAIAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAADgAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJCWVkZ2VBbHBoYSAqPSBpbm5lckFscGhhOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAGAAAAAYAAFYAAQAAAADZAAAAAAYAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAHoGAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBhbHBoYTsKCUBzd2l0Y2ggKDMpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueCksIDAuMCk7CgkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMC55KSwgMC4wKTsKCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7Cgl9CglAaWYgKDMgPT0gMiB8fCAzID09IDMpIAoJewoJCWFscGhhID0gMS4wIC0gYWxwaGE7Cgl9CgloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJX291dHB1dCA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCkgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAIAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAOACAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQloYWxmIGRpc3RhbmNlVG9Jbm5lckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqIChkIC0gY2lyY2xlRWRnZS53KSk7CgkJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgkJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZACAACBYAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAAYAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAAAAAAAMAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAAYAAAAFIACAAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAPsNAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2SGFpclF1YWRFZGdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAOAAAAaW5IYWlyUXVhZEVkZ2UAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAGABAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZACAACBUAAAAYAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAABQACKAACQAAAAEAADQAAIAAAAAKAAHQAAIAAAAAAAAAAAGQACAAAEAAAAAAAAAAAAAPAAHQADYAB4AAAAAACMABAAABAAAAAAAAAAAABAAAAALQAFQAA":"AgAAAExTS1OYAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAIAJAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1c3RhcnRfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHVlbmRfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7Cglfb3V0cHV0ID0gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MxLCB0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkRWRnZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJCWhhbGYyIGR1dmR5ID0gaGFsZjIoZEZkeSh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJCXsKCQkJZWRnZUFscGhhID0gbWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQloYWxmMiBnRiA9IGhhbGYyKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeS54IC0gZHV2ZHkueSk7CgkJCWVkZ2VBbHBoYSA9ICh2UXVhZEVkZ2VfU3RhZ2UwLngqdlF1YWRFZGdlX1N0YWdlMC54IC0gdlF1YWRFZGdlX1N0YWdlMC55KTsKCQkJZWRnZUFscGhhID0gc2F0dXJhdGUoMC41IC0gZWRnZUFscGhhIC8gbGVuZ3RoKGdGKSk7CgkJfQoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","CAZACAECBUAABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAOAAMAAAUAAAABEABCQACAAAAACYAB4AACAAAAAAAAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAUAAIAAAIAAAAAAAAAAAAIAAAADAABMAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAAyggAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTAxX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMwMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTIzX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZiB1dGhyZXNob2xkX1N0YWdlMV9jMF9jMTsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihsZW5ndGgodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwKSk7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBEdWFsSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAodCA8IHV0aHJlc2hvbGRfU3RhZ2UxX2MwX2MxKSAKCXsKCQlzY2FsZSA9IHVzY2FsZTAxX1N0YWdlMV9jMF9jMTsKCQliaWFzID0gdWJpYXMwMV9TdGFnZTFfYzBfYzE7Cgl9CgllbHNlIAoJewoJCXNjYWxlID0gdXNjYWxlMjNfU3RhZ2UxX2MwX2MxOwoJCWJpYXMgPSB1YmlhczIzX1N0YWdlMV9jMF9jMTsKCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IER1YWxJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKGZhbHNlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBhbHBoYSA9IDEuMDsKCQlhbHBoYSA9IHZpbkNvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZACAECCAAAAAAAAAACOAIAAAJQAAIACIAAAAH777776EYAAEAP777777777777EAAFWAAAAAAAAAYAAAACYACSAACQAAAAGQAEKAAIAAAAAPAAHQAAIAAAAAAAAAAAAMAAAACMAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAAYAAAAGQACAAACAAAAAAAAAAAACAAAAA6AALAAA":"AgAAAExTS1PWAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y292ZXJhZ2VfU3RhZ2UwID0gY292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAAFUMAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMTsKaW4gZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKGxlbmd0aCh2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApKTsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKDMgPD0gNCB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLncpIAoJewoJCWlmICgzIDw9IDIgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS55KSAKCQl7CgkJCWlmICgzIDw9IDEgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSAzIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueikgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoMyA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDMgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQgY292ZXJhZ2UgPSB2Y292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGhhbGYoY292ZXJhZ2UpKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZACAACB4AABAIYAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAACAAAAAEAAFEAAFAAAAAKAAIUAAQAAAAAYAAPAAAQAAAAAAAAAAABAAAAAEAABAAACAAAAAAAAAAAAAHQADYAB4AA6AAAAAAACAAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1NhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAAAA3AwAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTZfN19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzNl83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMTsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihsZW5ndGgodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwKSk7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBVbnJvbGxlZEJpbmFyeUdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCWZsb2F0NCBzY2FsZSwgYmlhczsKCWlmICg0IDw9IDQgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS53KSAKCXsKCQlpZiAoNCA8PSAyIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoNCA8PSAxIHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEueCkgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDQgPD0gMyB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlNl83X1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczZfN19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICg0IDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoNCA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoNCA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCWhhbGYgYWxwaGEgPSAxLjA7CgkJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZAAAMCBEAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAOoCAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZACAECBYAAAAYAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAAAIAAAAAOAAUQAAUAAAABEAA4AACAAAAACYAB4AACAAAAAAAAAAAAEAAAAAPAAEAAAIAAAAAAAAAAAAA6AAPAAHQADYAAAAAAAIAAAABMAAQAAAQAAAAAAAAAAAAQAAAAGQACYAA":"AgAAAExTS1OYAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAMEOAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdXNjYWxlNl83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM2XzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoNCA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDQgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDQgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICg0IDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTZfN19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXM2XzdfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoNCA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDQgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDQgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJCWlmICh2UXVhZEVkZ2VfU3RhZ2UwLnogPiAwLjAgJiYgdlF1YWRFZGdlX1N0YWdlMC53ID4gMC4wKSAKCQl7CgkJCWVkZ2VBbHBoYSA9IG1pbihtaW4odlF1YWRFZGdlX1N0YWdlMC56LCB2UXVhZEVkZ2VfU3RhZ2UwLncpICsgMC41LCAxLjApOwoJCX0KCQllbHNlIAoJCXsKCQkJaGFsZjIgZ0YgPSBoYWxmMigyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR4LnggLSBkdXZkeC55LCAgICAgICAgICAgICAgIDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpOwoJCQllZGdlQWxwaGEgPSAodlF1YWRFZGdlX1N0YWdlMC54KnZRdWFkRWRnZV9TdGFnZTAueCAtIHZRdWFkRWRnZV9TdGFnZTAueSk7CgkJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAACAUAABEAAAAABGAABAAOAAAYABQAEIAAAAAAAAAAAAAAAEAAAAAOAAWAA":"AgAAAExTS1MxAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABjAwAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdUNvdmVyYWdlX1N0YWdlMDsKaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh1Q292ZXJhZ2VfU3RhZ2UwICogZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADgAAAGluSGFpclF1YWRFZGdlAAABAAAAAAAAAA==","CAZACAMCCIAAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAAAAAAACYACKAACQAAAAGQAEKAAIAAAAAPAAHQAAIAAAAAAQAAAAJAACAAAEAAAAABQAAAAACAAAABMAAAQAAQAAAAAAAAAAAAAAAAAGIACPAAEAAAAAAAAAAAAAAAAAA5AAHQAAIAAAAAAAAAAAAIAAAAEIABMAA":"AgAAAExTS1NIAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTI7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMV9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzFfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTIpKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAB1CAAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMjsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UyOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihsZW5ndGgodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwKSk7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gUmFkaWFsR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UyX2MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMiwgdlRyYW5zZm9ybWVkQ29vcmRzXzFfU3RhZ2UwKS5ycnJyOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBCbGVuZAoJCS8vIEJsZW5kIG1vZGU6IERzdEluIChDb21wb3NlLU9uZSBiZWhhdmlvcikKCQlvdXRwdXRfU3RhZ2UxID0gYmxlbmRfZHN0X2luKG91dHB1dENvbG9yX1N0YWdlMCwgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSkpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMjsKCXsKCQkvLyBTdGFnZSAyLCBNYXRyaXhFZmZlY3QKCQlvdXRwdXRfU3RhZ2UyID0gVGV4dHVyZUVmZmVjdF9TdGFnZTJfYzEob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0X1N0YWdlMjsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAICBIAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAQEAAAAAAAABAQDCAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAAAQAAAAGQAB6AAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAcQQAAHVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQyIHRleENvb3JkOwoJCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkgKiBvdXRwdXRDb2xvcl9TdGFnZTApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVFZmZlY3QKCQlmbG9hdDIgcHJldkNlbnRlcjsKCQlmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJCWhhbGYgZDsKCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TdGFnZTEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTEudykgLSAxLjApICogdWNpcmNsZV9TdGFnZTEueik7CgkJfQoJCWVsc2UgCgkJewoJCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlAaWYgKDEgPT0gMSB8fCAxID09IDMpIAoJCXsKCQkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBjbGFtcChkLCAwLjAsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gZCA+IDAuNSA/IGlucHV0Q29sb3IgOiBoYWxmNCgwLjApOwoJCX0KCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAABAAAAA6IAAAAEAAAXAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQBvBwAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAACAYAABAAIAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1M3AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAArAEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGFscGhhID0gMS4wOwoJCWFscGhhID0gdmluQ292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHWAAAAAYAABQAAQAAAADZAAAAA6YAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBALoEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7Cglfb3V0cHV0ID0gX2lucHV0ICogYWxwaGE7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKSAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZACAACCAAAAAAAAAAGOAQAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAGAAAAAYAAUQAAUAAAABYAA4AACAAAAAEAAB4AACAAAAAAEAAAAADAAAAAUAAEAAAIAAAAADAAAAAAEAAAAADAAAAAZAAAIAAIAAAAAAAAAAAAIAAAADUABMAA":"AgAAAExTS1OqAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAADYCwAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTJfM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVzY2FsZTRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMF8xX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXMyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczRfNV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoMyA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDMgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDMgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICgzIDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQl9Cgl9CgllbHNlIAoJewoJCWlmICgzIDw9IDYgfHwgdCA8IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzEueSkgCgkJewoJCQlpZiAoMyA8PSA1IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJCWVsc2UgCgkJewoJCQlpZiAoMyA8PSA3IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnopIAoJCQl7CgkJCQlzY2FsZSA9IGZsb2F0NCgwKTsKCQkJCWJpYXMgPSBmbG9hdDQoMCk7CgkJCX0KCQkJZWxzZSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJfQoJfQoJX291dHB1dCA9IGhhbGY0KGZsb2F0KHQpICogc2NhbGUgKyBiaWFzKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogRHN0SW4gKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9kc3RfaW4ob3V0cHV0Q29sb3JfU3RhZ2UwLCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpKSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAICCMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAAAAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAAAAAALQAEAAAEAAAAAAAAAAAAAAAAABUAATYABAAAAAAAAAAAAAAAAAAHQAB4AACAAAAAAAAAAAACAAAABDAALAAA":"AgAAAExTS1MCAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTI7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMV9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMikpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAxggAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTI7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMjsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKGZhbHNlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMl9jMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTIsIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMCkucnJycjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTI7Cgl7CgkJLy8gU3RhZ2UgMiwgTWF0cml4RWZmZWN0CgkJb3V0cHV0X1N0YWdlMiA9IFRleHR1cmVFZmZlY3RfU3RhZ2UyX2MxKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dF9TdGFnZTI7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAECBQAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAAFAAGAAAKAAAAAOAARIABAAAAABEAA6AABAAAAAAAAAAAAYAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAAAAAAJAAEAAAEAAAAAAAAAAAAEAAAABMAAWAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAE8KAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMDFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyM19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMjNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGYgdXRocmVzaG9sZF9TdGFnZTFfYzBfYzE7CmluIGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgRHVhbEludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpOwoJZmxvYXQ0IHNjYWxlLCBiaWFzOwoJaWYgKHQgPCB1dGhyZXNob2xkX1N0YWdlMV9jMF9jMSkgCgl7CgkJc2NhbGUgPSB1c2NhbGUwMV9TdGFnZTFfYzBfYzE7CgkJYmlhcyA9IHViaWFzMDFfU3RhZ2UxX2MwX2MxOwoJfQoJZWxzZSAKCXsKCQlzY2FsZSA9IHVzY2FsZTIzX1N0YWdlMV9jMF9jMTsKCQliaWFzID0gdWJpYXMyM19TdGFnZTFfYzBfYzE7Cgl9Cglfb3V0cHV0ID0gaGFsZjQoZmxvYXQodCkgKiBzY2FsZSArIGJpYXMpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBEdWFsSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQloYWxmMiBnRiA9IGhhbGYyKDIuMCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiBkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHkueCAtIGR1dmR5LnkpOwoJCWVkZ2VBbHBoYSA9IGhhbGYodkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggLSB2SGFpclF1YWRFZGdlX1N0YWdlMC55KTsKCQllZGdlQWxwaGEgPSBzcXJ0KGVkZ2VBbHBoYSAqIGVkZ2VBbHBoYSAvIGRvdChnRiwgZ0YpKTsKCQllZGdlQWxwaGEgPSBtYXgoMS4wIC0gZWRnZUFscGhhLCAwLjApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAAAAAAWAATYABAAAAAAAAAAAAAAAAAADYAB4AACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1OjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTEpKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAA5gIAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkucnJycjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgTWF0cml4RWZmZWN0CgkJb3V0cHV0X1N0YWdlMSA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZACAACBYAAAEIBAAABGAABAAOAAAYABQAEIAAAAAAAABAAAAABQACSAACQAAAAEAAEKAAIAAAAAKAAHQAAIAAAAAAAAAAAAQAAAABYAAQAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAABAAAAAFIACAAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1NbAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZAoJdkhhaXJRdWFkRWRnZV9TdGFnZTAgPSBpbkhhaXJRdWFkRWRnZTsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAGEOAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUwXzFfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGUyXzNfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1c2NhbGU2XzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczBfMV9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHViaWFzMl8zX1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBmbG9hdDQgdWJpYXM0XzVfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGZsb2F0NCB1YmlhczZfN19TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHV0aHJlc2hvbGRzOV8xM19TdGFnZTFfYzBfYzE7CmluIGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpOwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFJhZGlhbEdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgVW5yb2xsZWRCaW5hcnlHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7CglmbG9hdDQgc2NhbGUsIGJpYXM7CglpZiAoNCA8PSA0IHx8IHQgPCB1dGhyZXNob2xkczFfN19TdGFnZTFfYzBfYzEudykgCgl7CgkJaWYgKDQgPD0gMiB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDQgPD0gMSB8fCB0IDwgdXRocmVzaG9sZHMxXzdfU3RhZ2UxX2MwX2MxLngpIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTBfMV9TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXMwXzFfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gdXNjYWxlMl8zX1N0YWdlMV9jMF9jMTsKCQkJCWJpYXMgPSB1YmlhczJfM19TdGFnZTFfYzBfYzE7CgkJCX0KCQl9CgkJZWxzZSAKCQl7CgkJCWlmICg0IDw9IDMgfHwgdCA8IHV0aHJlc2hvbGRzMV83X1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSB1c2NhbGU0XzVfU3RhZ2UxX2MwX2MxOwoJCQkJYmlhcyA9IHViaWFzNF81X1N0YWdlMV9jMF9jMTsKCQkJfQoJCQllbHNlIAoJCQl7CgkJCQlzY2FsZSA9IHVzY2FsZTZfN19TdGFnZTFfYzBfYzE7CgkJCQliaWFzID0gdWJpYXM2XzdfU3RhZ2UxX2MwX2MxOwoJCQl9CgkJfQoJfQoJZWxzZSAKCXsKCQlpZiAoNCA8PSA2IHx8IHQgPCB1dGhyZXNob2xkczlfMTNfU3RhZ2UxX2MwX2MxLnkpIAoJCXsKCQkJaWYgKDQgPD0gNSB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS54KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCQllbHNlIAoJCXsKCQkJaWYgKDQgPD0gNyB8fCB0IDwgdXRocmVzaG9sZHM5XzEzX1N0YWdlMV9jMF9jMS56KSAKCQkJewoJCQkJc2NhbGUgPSBmbG9hdDQoMCk7CgkJCQliaWFzID0gZmxvYXQ0KDApOwoJCQl9CgkJCWVsc2UgCgkJCXsKCQkJCXNjYWxlID0gZmxvYXQ0KDApOwoJCQkJYmlhcyA9IGZsb2F0NCgwKTsKCQkJfQoJCX0KCX0KCV9vdXRwdXQgPSBoYWxmNChmbG9hdCh0KSAqIHNjYWxlICsgYmlhcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFVucm9sbGVkQmluYXJ5R3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZACAACBUAAAAYAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAABQACKAACQAAAAEAADQAAIAAAAAKAAHQAAIAAAAAAQAAAAGQACAAAEAAAAAAAAAAAAAPAAHQADYAB4AEAAAACMABAAABAAAAAAAAAAAABAAAAALQAFQAA":"AgAAAExTS1OYAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAH8JAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1c3RhcnRfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHVlbmRfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7Cglfb3V0cHV0ID0gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MxLCB0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","CAZACAACB4AAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1OvAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAYQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA=="}} \ No newline at end of file diff --git a/shaders_1.22.6.sksl.json b/shaders_1.22.6.sksl.json new file mode 100644 index 000000000..fa507ac3f --- /dev/null +++ b/shaders_1.22.6.sksl.json @@ -0,0 +1 @@ +{"platform":"android","name":"SM G970N","engineRevision":"2f0af3715217a0c2ada72c717d4ed9178d68f6ed","data":{"CAZACAECBMAAAAIACAAAAAAAAAAAAAAACMAACAA4AAIQB777777RQADSAAAAAAAAEAACOAAEAAAAAAAAAAAAAAAAAAYAAGYAAQAAAAANAAAAAAAAAAAEAAACAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1PNAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHV2aWV3TWF0cml4X1N0YWdlMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVmVydGljZXNHUAoJaGFsZjQgY29sb3IgPSBpbkNvbG9yOwoJY29sb3IgPSBjb2xvci5iZ3JhOwoJY29sb3IgPSBjb2xvcjsKCWNvbG9yID0gaGFsZjQoY29sb3IucmdiICogY29sb3IuYSwgY29sb3IuYSk7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX3Bvc2l0aW9uID0gdXZpZXdNYXRyaXhfU3RhZ2UwLnh6ICogcG9zaXRpb24gKyB1dmlld01hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfcG9zaXRpb24ueCAsIF90bXBfMF9wb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAAhBAAAdW5pZm9ybSBoYWxmNCB1Y29sb3JfU3RhZ2UxX2MwOwppbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDb25zdENvbG9yUHJvY2Vzc29yX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHVjb2xvcl9TdGFnZTFfYzA7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiBpbnB1dEFscGhhID0gX2lucHV0Lnc7CgloYWxmIGZhY3RvciA9IDEuMCAtIGlucHV0QWxwaGE7CglAc3dpdGNoICgwKSAKCXsKCQljYXNlIDA6ICAgICAgICBmYWN0b3IgPSBleHAoKC1mYWN0b3IgKiBmYWN0b3IpICogNC4wKSAtIDAuMDE3OTk5OTk5MjI1MTM5NjE4OwoJCWJyZWFrOwoJCWNhc2UgMTogICAgICAgIGZhY3RvciA9IHNtb290aHN0ZXAoMS4wLCAwLjAsIGZhY3Rvcik7CgkJYnJlYWs7Cgl9Cglfb3V0cHV0ID0gaGFsZjQoZmFjdG9yKTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVmVydGljZXNHUAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKFNrTW9kZSBiZWhhdmlvcikKCQlvdXRwdXRfU3RhZ2UxID0gYmxlbmRfbW9kdWxhdGUoQ29uc3RDb2xvclByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQoMSkpLCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShvdXRwdXRDb2xvcl9TdGFnZTApKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24HAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAACBAAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAArQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECA4AAAAIAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAOACAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQloYWxmIGRpc3RhbmNlVG9Jbm5lckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqIChkIC0gY2lyY2xlRWRnZS53KSk7CgkJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgkJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAMCBEAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAOoCAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAMCBMAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAABAAAAA6IAAAAEAAAXAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQBvBwAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAACBAAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAFgIAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAACAYAABEAJAAABGAABAAOAAEIA777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAABVAQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZAAAACAYAAAAAIAAABGAABAD7777777777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1PkAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAWgEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAEAAAAKAAAAaW5Qb3NpdGlvbgAAAQAAAAAAAAA=","CAZAAAECA4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABMAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAACA4AAAAYAAAAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALQAAAAAAAAAAAAAAAQAAAACYACYAA":"AgAAAExTS1PjAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNEaW1lbnNpb25zSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSAodGV4SWR4KTsKCXZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAXAwAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBmbG9hdDIgdkludFRleHR1cmVDb29yZHNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGlzdGFuY2VGaWVsZFBhdGgKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfU3RhZ2UwOwoJCWhhbGY0IHRleENvbG9yOwoJCXsKCQkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dikucnJycjsKCQl9CgkJaGFsZiBkaXN0YW5jZSA9IDcuOTY4NzUqKHRleENvbG9yLnIgLSAwLjUwMTk2MDc4NDMxKTsKCQloYWxmIGFmd2lkdGg7CgkJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeSh2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTAueSkpKTsKCQloYWxmIHZhbCA9IHNtb290aHN0ZXAoLWFmd2lkdGgsIGFmd2lkdGgsIGRpc3RhbmNlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh2YWwpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACAYAABAAIAAABGAABAD777777777777YZAAAAAFAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1M3AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAArAEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCQloYWxmIGFscGhhID0gMS4wOwoJCWFscGhhID0gdmluQ292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAMCBEAAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHSAAAAAYAABQAAQAAAAAAAAAAAAQAAAAEAACYAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAEBAAAAAAAAAQEA5QIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAACBAAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAA4AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAQEAAAAAAAABAQDCAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAECA4AAAAIAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAADgAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJCWVkZ2VBbHBoYSAqPSBpbm5lckFscGhhOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAECA4AAAAAAAAABKAADAAKQAAYACUAAGAATAAAQAFIAAMABKAADAAOAAEIAEAADEAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1MXBAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgcmFkaWk7CglyYWRpaS54ID0gZG90KHJhZGlpX3NlbGVjdG9yLCByYWRpaV94KTsKCXJhZGlpLnkgPSBkb3QocmFkaWlfc2VsZWN0b3IsIHJhZGlpX3kpOwoJYm9vbCBpc19hcmNfc2VjdGlvbiA9IChyYWRpaS54ID4gMCk7CglyYWRpaSA9IGFicyhyYWRpaSk7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpOwoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZTsKCWlmIChpc19hcmNfc2VjdGlvbikgCgl7CgkJdmFyY2Nvb3JkX1N0YWdlMC54eSA9IDEgLSBhYnMocmFkaXVzX291dHNldCk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqICh2YXJjY29vcmRfU3RhZ2UwLnh5L3JhZGlpICogY29ybmVyICogMik7Cgl9CgllbHNlIAoJewoJCXZhcmNjb29yZF9TdGFnZTAgPSBmbG9hdDQoMCk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAACgCAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0NCB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgkJaWYgKGZsb2F0MigwKSAhPSB2YXJjY29vcmRfU3RhZ2UwLnh5KSAKCQl7CgkJCWZsb2F0IGZuID0gZG90KHZhcmNjb29yZF9TdGFnZTAueHksIHZhcmNjb29yZF9TdGFnZTAueHkpIC0gMTsKCQkJaWYgKGZuID4gMCkgCgkJCXsKCQkJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDApOwoJCQl9CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAHAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECAUAAAEYAAEABYAARAANQAAQAAAAAAAAMABEQAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1OFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFJSZWN0U2hhZG93Cgl2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAB1AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUlJlY3RTaGFkb3cKCQloYWxmMyBzaGFkb3dQYXJhbXM7CgkJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CgkJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJCWhhbGYgZmFjdG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdXYpLnJycnIuYTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChmYWN0b3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","CAZAAAECAUAAAAAAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAAAAAAAAABAAAAAEAAFQAA":"AgAAAExTS1NuAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAB+AwAAaW4gaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAAAAAIIDAAWAATYABEAAAAABAAAAAABBAMADYAB4AACQAAAABQAAAAABAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABEAAAAABAAAQAABBAMADYAB4AACQAAAABQAAAAABAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAshQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gY2xhbXAoc3Vic2V0Q29vcmQueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCV9vdXRwdXQgPSB0ZXh0dXJlQ29sb3I7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAICBIAAAAIAAAABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1M6CQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAdQQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJCWZsb2F0IGZud2lkdGggPSBhYnMoZ3gpICsgYWJzKGd5KTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAACBUAAAAYAAAABGAABAAOAAEIADQAAGAAQABNAAAAAAAABQACKAACQAAAAEAADQAAIAAAAAKAAHQAAIAAAAAAQAAAAGQACAAAEAAAAAAAAAAAAAPAAHQADYAB4AEAAAACMABAAABAAAAAAAAAAAABAAAAALQAFQAA":"AgAAAExTS1OYAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAH8JAAB1bmlmb3JtIGhhbGY0IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1c3RhcnRfU3RhZ2UxX2MwX2MxOwp1bmlmb3JtIGhhbGY0IHVlbmRfU3RhZ2UxX2MwX2MxOwppbiBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKF9jb29yZHMueCk7Cglfb3V0cHV0ID0gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MxLCB0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","CAZACAECBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABAAAAAAAAAAAAABBAMADYAB4AACAAAAAAAAAAAANAAAAAAAAAAAAAIIDABKAAAQAAQAAAAAAAAAAAAQAAAAGQACYAA":"AgAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAACIAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpKSwgb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAoAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAGAAAAAYAAFYAAQAAAADZAAAAAAYAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAHoGAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBhbHBoYTsKCUBzd2l0Y2ggKDMpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueCksIDAuMCk7CgkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMC55KSwgMC4wKTsKCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7Cgl9CglAaWYgKDMgPT0gMiB8fCAzID09IDMpIAoJewoJCWFscGhhID0gMS4wIC0gYWxwaGE7Cgl9CgloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJX291dHB1dCA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCkgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAEAAAIIDAAWAATYABEAAAAAAAAAQAABBAMADYAB4AACQAAAABQAAAAAAAAAQAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAfRQAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACAUAABEAAAAABGAABAAOAAAYABQAEIAAAAAAAAAAAAAAAEAAAAAOAAWAA":"AgAAAExTS1MxAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABjAwAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdUNvdmVyYWdlX1N0YWdlMDsKaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh1Q292ZXJhZ2VfU3RhZ2UwICogZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADgAAAGluSGFpclF1YWRFZGdlAAABAAAAAAAAAA==","CAZAAAACBAAAADQBAEACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAABAAAAAGQAFQAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAA5gYAAHVuaWZvcm0gaGFsZiB1U3JjVEZfU3RhZ2UwWzddOwp1bmlmb3JtIGhhbGYzeDMgdUNvbG9yWGZvcm1fU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdURzdFRGX1N0YWdlMFs3XTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmIHNyY190Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdVNyY1RGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVTcmNURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1U3JjVEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdVNyY1RGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVTcmNURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1U3JjVEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdVNyY1RGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmIGRzdF90Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdURzdFRGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVEc3RURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1RHN0VEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdURzdFRGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVEc3RURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1RHN0VEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdURzdFRGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmNCBnYW11dF94Zm9ybV9TdGFnZTAoaGFsZjQgY29sb3IpIAp7Cgljb2xvci5yZ2IgPSAodUNvbG9yWGZvcm1fU3RhZ2UwICogY29sb3IucmdiKTsKCXJldHVybiBjb2xvcjsKfQpoYWxmNCBjb2xvcl94Zm9ybV9TdGFnZTAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IuciA9IHNyY190Zl9TdGFnZTAoaGFsZihjb2xvci5yKSk7Cgljb2xvci5nID0gc3JjX3RmX1N0YWdlMChoYWxmKGNvbG9yLmcpKTsKCWNvbG9yLmIgPSBzcmNfdGZfU3RhZ2UwKGhhbGYoY29sb3IuYikpOwoJY29sb3IgPSBnYW11dF94Zm9ybV9TdGFnZTAoaGFsZjQoY29sb3IpKTsKCWNvbG9yLnIgPSBkc3RfdGZfU3RhZ2UwKGhhbGYoY29sb3IucikpOwoJY29sb3IuZyA9IGRzdF90Zl9TdGFnZTAoaGFsZihjb2xvci5nKSk7Cgljb2xvci5iID0gZHN0X3RmX1N0YWdlMChoYWxmKGNvbG9yLmIpKTsKCXJldHVybiBoYWxmNChjb2xvcik7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKGNvbG9yX3hmb3JtX1N0YWdlMChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkpICogb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAACAYAAAEAYAAABGAABAAOAAEIA7777777777776FAABYAAAAAAAAAAAAAAAIAAAABEABMAA":"AgAAAExTS1NnAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBjb2xvciA9IGluQ29sb3I7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAVQEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZACAECBMAAAAIAAAAAAAAAAAAAAAAACMAACAA4AAIQB777777RQADSAAAAAAAAEAACOAAEAAAAAAAAAAAAAAAAAAYAAGYAAQAAAAANAAAAAAAAAAAEAAACAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1N6AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFZlcnRpY2VzR1AKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IuYmdyYTsKCWNvbG9yID0gY29sb3I7Cgljb2xvciA9IGhhbGY0KGNvbG9yLnJnYiAqIGNvbG9yLmEsIGNvbG9yLmEpOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9wb3NpdGlvbiA9IHBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX3Bvc2l0aW9uLnggLCBfdG1wXzBfcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAhBAAAdW5pZm9ybSBoYWxmNCB1Y29sb3JfU3RhZ2UxX2MwOwppbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDb25zdENvbG9yUHJvY2Vzc29yX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IHVjb2xvcl9TdGFnZTFfYzA7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiBpbnB1dEFscGhhID0gX2lucHV0Lnc7CgloYWxmIGZhY3RvciA9IDEuMCAtIGlucHV0QWxwaGE7CglAc3dpdGNoICgwKSAKCXsKCQljYXNlIDA6ICAgICAgICBmYWN0b3IgPSBleHAoKC1mYWN0b3IgKiBmYWN0b3IpICogNC4wKSAtIDAuMDE3OTk5OTk5MjI1MTM5NjE4OwoJCWJyZWFrOwoJCWNhc2UgMTogICAgICAgIGZhY3RvciA9IHNtb290aHN0ZXAoMS4wLCAwLjAsIGZhY3Rvcik7CgkJYnJlYWs7Cgl9Cglfb3V0cHV0ID0gaGFsZjQoZmFjdG9yKTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVmVydGljZXNHUAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEJsZW5kCgkJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKFNrTW9kZSBiZWhhdmlvcikKCQlvdXRwdXRfU3RhZ2UxID0gYmxlbmRfbW9kdWxhdGUoQ29uc3RDb2xvclByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQoMSkpLCBCbHVycmVkRWRnZUZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMShvdXRwdXRDb2xvcl9TdGFnZTApKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24HAAAAaW5Db2xvcgABAAAAAAAAAA==","CAZAAAICBIAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAZAAAAAAAAAAAAAAB4QAAAAGQAAMAAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1PQCAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEANwQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IG91dHB1dENvdmVyYWdlX1N0YWdlMCAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAMCBAAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAABAAAAAKAAC4AAIAAAAAAAAAAAAIAAAABYABMAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAQEAAAAAAAABAQCCBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","CAZAAAMCBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAHWAAAAAYAABQAAQAAAADZAAAAA6YAAAAEAAAGAACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBALoEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7Cglfb3V0cHV0ID0gX2lucHV0ICogYWxwaGE7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKSAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZACAACB4AAAAAAAAAGOAIAAAJQAAIACIAAAAA4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAKAAAABWAAWAA":"AgAAAExTS1McAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBwb3NpdGlvbiA9IHBvc2l0aW9uLnh5OwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJdmNvdmVyYWdlX1N0YWdlMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAvQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3IKCQloYWxmNCBjb25zdENvbG9yOwoJCUBpZiAoZmFsc2UpIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJCX0KCQllbHNlIAoJCXsKCQkJY29uc3RDb2xvciA9IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKTsKCQl9CgkJb3V0cHV0X1N0YWdlMSA9IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzAoY29uc3RDb2xvcik7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfU3RhZ2UxKSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAECBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAIAAEAAAIIDAAWAATYABAAAAAABAAAQAABBAMADYAB4AACAAAAAAAAAAAACAAAAAUAALAAA":"AgAAAExTS1NdAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAACcEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwLnksIHVjbGFtcF9TdGFnZTFfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJX291dHB1dCA9IHRleHR1cmVDb2xvcjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE1hdHJpeEVmZmVjdAoJCW91dHB1dF9TdGFnZTEgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAICBIAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAQQGABAABNQAAAAAAAAAAAAAAAQAAAAGQAB6AAEAAAAAAAAAAAAEAAAABCAAWAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAcQQAAHVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQyIHRleENvb3JkOwoJCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkgKiBvdXRwdXRDb2xvcl9TdGFnZTApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVFZmZlY3QKCQlmbG9hdDIgcHJldkNlbnRlcjsKCQlmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJCWhhbGYgZDsKCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TdGFnZTEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTEudykgLSAxLjApICogdWNpcmNsZV9TdGFnZTEueik7CgkJfQoJCWVsc2UgCgkJewoJCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlAaWYgKDEgPT0gMSB8fCAxID09IDMpIAoJCXsKCQkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBjbGFtcChkLCAwLjAsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gZCA+IDAuNSA/IGlucHV0Q29sb3IgOiBoYWxmNCgwLjApOwoJCX0KCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAECAYAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAA2AAAAAAAAAAAAAAACAAAAAKAALAAA":"AgAAAExTS1NGAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVGV4dHVyZQoJaW50IHRleElkeCA9IDA7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gKHRleElkeCk7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAZAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IGluIGludCB2VGV4SW5kZXhfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKS5ycnJyOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZACAACBYAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFWAAAAAAAAAAAAAAAAIIDAAWAATYABEAAAAAAAAAAAABBAMADYAB4AACQAAAABQAAAAAAAAAAAABBAMAFAABTAACAAAAAAAAAAAACAAAAAZAALAAA":"AgAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAOxMAAHVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TdGFnZTE7CnVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFbN107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgX2Nvb3Jkcyk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgR2F1c3NpYW5Db252b2x1dGlvbgoJCWZsb2F0MiBfY29vcmRzID0gdkxvY2FsQ29vcmRfU3RhZ2UwLnh5OwoJCW91dHB1dF9TdGFnZTEgPSBoYWxmNCgwLCAwLCAwLCAwKTsKCQlmbG9hdDIgY29vcmQgPSBfY29vcmRzIC0gMTIuMCAqIHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWZsb2F0MiBjb29yZFNhbXBsZWQgPSBoYWxmMigwLCAwKTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAWwEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZACAACB4AAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFWAAAAAAAAKAAJIAAKAAAAAYAAOAABAAAAABYAA6AABAAAAAACAAAABCAAIAAAQAAAAAAAAAAAAB4AA6AAPAAHQAQAAAALQAEAAAEAAAAAAAAAAAAEAAAABWAAWAA":"AgAAAExTS1OvAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAYQcAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDY7Cglfb3V0cHV0ID0gaGFsZjQodCwgMS4wLCAwLjAsIDAuMCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYoX2Nvb3Jkcy54KTsKCV9vdXRwdXQgPSBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzEsIHQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCV9vdXRwdXQgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCV9vdXRwdXQgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7Cgl9CgllbHNlIAoJewoJCV9vdXRwdXQgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCV9vdXRwdXQueHl6ICo9IF9vdXRwdXQudzsKCX0KCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yCgkJaGFsZjQgY29uc3RDb2xvcjsKCQlAaWYgKGZhbHNlKSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgwKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWNvbnN0Q29sb3IgPSBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCk7CgkJfQoJCW91dHB1dF9TdGFnZTEgPSBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGNvbnN0Q29sb3IpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAECA4AAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFWAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAGABAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAAAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAKAAAAAAAAAAAAAAAEAAAAAYAAWAA":"AgAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAEwCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA=="}} \ No newline at end of file From a85612269ace3c097691464d261395a5587be612 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Jan 2021 17:46:51 +0900 Subject: [PATCH 30/44] various fixes --- lib/model/source/media_store_source.dart | 3 +-- lib/services/image_file_service.dart | 1 - lib/widgets/common/grid/sliver.dart | 2 +- lib/widgets/search/expandable_filter_row.dart | 3 ++- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index a52e78b80..838ddd9e7 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -106,8 +106,7 @@ class MediaStoreSource extends CollectionSource { } Future refreshUris(List changedUris) async { - assert(_initialized); - debugPrint('$runtimeType refreshUris uris=$changedUris'); + if (!_initialized) return; final uriByContentId = Map.fromEntries(changedUris.map((uri) { if (uri == null) return null; diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 31a67829b..723d1312e 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -57,7 +57,6 @@ class ImageFileService { } static Future getEntry(String uri, String mimeType) async { - debugPrint('getEntry for uri=$uri, mimeType=$mimeType'); try { final result = await platform.invokeMethod('getEntry', { 'uri': uri, diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart index 4c74a1688..85ba6b917 100644 --- a/lib/widgets/common/grid/sliver.dart +++ b/lib/widgets/common/grid/sliver.dart @@ -264,7 +264,7 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { final targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint) : null; geometry = SliverGeometry( scrollExtent: estimatedMaxScrollOffset, - paintExtent: paintExtent, + paintExtent: math.min(paintExtent, estimatedMaxScrollOffset), cacheExtent: cacheExtent, maxPaintExtent: estimatedMaxScrollOffset, // Conservative to avoid flickering away the clip during scroll. diff --git a/lib/widgets/search/expandable_filter_row.dart b/lib/widgets/search/expandable_filter_row.dart index 5702fc9fb..a838d3901 100644 --- a/lib/widgets/search/expandable_filter_row.dart +++ b/lib/widgets/search/expandable_filter_row.dart @@ -105,7 +105,8 @@ class ExpandableFilterRow extends StatelessWidget { Widget _buildFilterChip(CollectionFilter filter) { return AvesFilterChip( - key: ValueKey(filter), + // key `album-...` is expected by test driver + key: Key(filter.key), filter: filter, heroType: heroTypeBuilder?.call(filter) ?? HeroType.onTap, onTap: onTap, From 8b1d37fc32c5af792db3b3b5db94302144e0d68d Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Jan 2021 18:57:38 +0900 Subject: [PATCH 31/44] only geolocate and show maps when connected --- lib/model/connectivity.dart | 28 ++++++++ lib/model/source/location.dart | 5 +- lib/utils/constants.dart | 14 ++-- lib/widgets/home_page.dart | 10 ++- lib/widgets/viewer/entry_vertical_pager.dart | 9 ++- lib/widgets/viewer/entry_viewer_stack.dart | 12 +++- lib/widgets/viewer/info/location_section.dart | 70 +++++++++++-------- pubspec.lock | 28 ++++++++ pubspec.yaml | 1 + 9 files changed, 137 insertions(+), 40 deletions(-) create mode 100644 lib/model/connectivity.dart diff --git a/lib/model/connectivity.dart b/lib/model/connectivity.dart new file mode 100644 index 000000000..009c0fa1c --- /dev/null +++ b/lib/model/connectivity.dart @@ -0,0 +1,28 @@ +import 'package:connectivity/connectivity.dart'; +import 'package:flutter/foundation.dart'; + +final AvesConnectivity connectivity = AvesConnectivity._private(); + +class AvesConnectivity { + bool _isConnected; + + AvesConnectivity._private() { + Connectivity().onConnectivityChanged.listen(_updateFromResult); + } + + void onResume() => _isConnected = null; + + Future get isConnected async { + if (_isConnected != null) return SynchronousFuture(_isConnected); + final result = await (Connectivity().checkConnectivity()); + _updateFromResult(result); + return _isConnected; + } + + Future get canGeolocate => isConnected; + + void _updateFromResult(ConnectivityResult result) { + _isConnected = result != ConnectivityResult.none; + debugPrint('Device is connected=$_isConnected'); + } +} diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index 16de86443..37d6c0af8 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -1,7 +1,8 @@ import 'dart:math'; -import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/connectivity.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/location.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -27,6 +28,8 @@ mixin LocationMixin on SourceBase { } Future locateEntries() async { + if (!(await connectivity.canGeolocate)) return; + // final stopwatch = Stopwatch()..start(); final byLocated = groupBy(rawEntries.where((entry) => entry.hasGps), (entry) => entry.isLocated); final todo = byLocated[false] ?? []; diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 51597f019..87453882c 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -29,14 +29,14 @@ class Constants { Dependency( name: 'AndroidX Core-KTX', license: 'Apache 2.0', - licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/LICENSE.txt', - sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/core/core-ktx', + licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt', + sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/core/core-ktx', ), Dependency( name: 'AndroidX Exifinterface', license: 'Apache 2.0', - licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/LICENSE.txt', - sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/exifinterface/exifinterface', + licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt', + sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/exifinterface/exifinterface', ), Dependency( name: 'Android-TiffBitmapFactory', @@ -83,6 +83,12 @@ class Constants { licenseUrl: 'https://github.com/dart-lang/collection/blob/master/LICENSE', sourceUrl: 'https://github.com/dart-lang/collection', ), + Dependency( + name: 'Connectivity', + 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', + ), Dependency( name: 'Decorated Icon', license: 'MIT', diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 08346aa82..0cff14049 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -1,4 +1,5 @@ import 'package:aves/main.dart'; +import 'package:aves/model/connectivity.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/home_page.dart'; @@ -111,9 +112,14 @@ class _HomePageState extends State { Future _initViewerEntry({@required String uri, @required String mimeType}) async { final entry = await ImageFileService.getEntry(uri, mimeType); if (entry != null) { - // cataloguing is essential for geolocation and video rotation + // cataloguing is essential for coordinates and video rotation await entry.catalog(); - unawaited(entry.locate()); + // locating is fine in the background + unawaited(connectivity.canGeolocate.then((connected) { + if (connected) { + entry.locate(); + } + })); } return entry; } diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 95391d6a9..2abf301a7 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:aves/model/connectivity.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; @@ -150,8 +151,12 @@ class _ViewerVerticalPageViewState extends State { entry.imageChangeNotifier.addListener(_onImageChanged); // make sure to locate the entry, // so that we can display the address instead of coordinates - // even when background locating has not reached this entry yet - entry.locate(); + // even when initial collection locating has not reached this entry yet + connectivity.canGeolocate.then((connected) { + if (connected) { + entry.locate(); + } + }); } else { Navigator.pop(context); } diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 7ede8f873..f9eec2f2d 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:aves/model/connectivity.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/screen_on.dart'; @@ -141,8 +142,15 @@ class _EntryViewerStackState extends State with SingleTickerPr @override void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.paused) { - _pauseVideoControllers(); + switch (state) { + case AppLifecycleState.paused: + _pauseVideoControllers(); + break; + case AppLifecycleState.resumed: + connectivity.onResume(); + break; + default: + break; } } diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index bbf521cc2..1dd7a3e0c 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -1,5 +1,6 @@ -import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/connectivity.dart'; 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/map_style.dart'; import 'package:aves/model/settings/settings.dart'; @@ -102,34 +103,40 @@ class _LocationSectionState extends State with TickerProviderSt crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.showTitle) SectionRow(AIcons.location), - NotificationListener( - onNotification: (notification) { - if (notification is MapStyleChangedNotification) setState(() {}); - return false; + FutureBuilder( + future: connectivity.isConnected, + builder: (context, snapshot) { + if (snapshot.data != true) return SizedBox(); + return NotificationListener( + onNotification: (notification) { + if (notification is MapStyleChangedNotification) setState(() {}); + return false; + }, + child: 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: 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, - ), - ), ), if (entry.hasGps) _AddressInfoGroup(entry: entry), if (filters.isNotEmpty) @@ -174,7 +181,12 @@ class _AddressInfoGroupState extends State<_AddressInfoGroup> { @override void initState() { super.initState(); - _addressLineLoader = entry.findAddressLine(); + _addressLineLoader = connectivity.canGeolocate.then((connected) { + if (connected) { + return entry.findAddressLine(); + } + return null; + }); } @override diff --git a/pubspec.lock b/pubspec.lock index b3b97f713..97fe07ccf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0-nullsafety.3" + connectivity: + dependency: "direct main" + description: + name: connectivity + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + connectivity_for_web: + dependency: transitive + description: + name: connectivity_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+4" + connectivity_macos: + dependency: transitive + description: + name: connectivity_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+7" + connectivity_platform_interface: + dependency: transitive + description: + name: connectivity_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.6" console_log_handler: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 63fee4179..7dbfd751f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: sdk: flutter charts_flutter: collection: + connectivity: decorated_icon: event_bus: expansion_tile_card: From 10b4ce6898aa1937262a34028f69e36bdfd2ebd5 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Jan 2021 19:24:25 +0900 Subject: [PATCH 32/44] fixed scroll offset after scaling when there are no section headers --- lib/widgets/common/grid/section_layout.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart index a49a2e241..70be8fd0c 100644 --- a/lib/widgets/common/grid/section_layout.dart +++ b/lib/widgets/common/grid/section_layout.dart @@ -167,7 +167,7 @@ class SectionedListLayout { final sectionItemIndex = section.value.indexOf(item); final column = sectionItemIndex % columnCount; final row = (sectionItemIndex / columnCount).floor(); - final listIndex = sectionLayout.firstIndex + (showHeaders ? 1 : 0) + row; + final listIndex = sectionLayout.firstIndex + 1 + row; final left = tileExtent * column + spacing * (column - 1); final top = sectionLayout.indexToLayoutOffset(listIndex); From c7b6e17a7fe0029798ff19588384cf589a0c6586 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 27 Jan 2021 21:16:43 +0900 Subject: [PATCH 33/44] print: show feedback for multipage entries --- lib/model/source/media_store_source.dart | 4 +++- .../collection/entry_set_action_delegate.dart | 8 +++---- .../common/action_mixins/feedback.dart | 18 +++++++--------- .../common/chip_action_delegate.dart | 8 +++---- lib/widgets/viewer/entry_action_delegate.dart | 6 +++--- lib/widgets/viewer/printer.dart | 21 ++++++++++++++----- 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 838ddd9e7..ea0cd40f2 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -111,7 +111,9 @@ class MediaStoreSource extends CollectionSource { final uriByContentId = Map.fromEntries(changedUris.map((uri) { if (uri == null) return null; final idString = Uri.parse(uri).pathSegments.last; - return MapEntry(int.tryParse(idString), uri); + final contentId = int.tryParse(idString); + if (contentId == null) return null; + return MapEntry(contentId, uri); }).where((kv) => kv != null)); // clean up obsolete entries diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 36d96fcd2..6c3276361 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -79,14 +79,14 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, moveType)) return; final copy = moveType == MoveType.copy; + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum), + itemCount: selectionCount, onDone: (processed) async { final movedOps = processed.where((e) => e.success); final movedCount = movedOps.length; - final selectionCount = selection.length; if (movedCount < selectionCount) { final count = selectionCount - movedCount; showFeedback(context, 'Failed to ${copy ? 'copy' : 'move'} ${Intl.plural(count, one: '$count item', other: '$count items')}'); @@ -132,14 +132,14 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (!await checkStoragePermission(context, selection)) return; + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.delete(selection), + itemCount: selectionCount, onDone: (processed) { final deletedUris = processed.where((e) => e.success).map((e) => e.uri).toList(); final deletedCount = deletedUris.length; - final selectionCount = selection.length; if (deletedCount < selectionCount) { final count = selectionCount - deletedCount; showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 3a0120985..4a1743e68 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -1,5 +1,3 @@ -import 'package:aves/model/entry.dart'; -import 'package:aves/services/image_op_events.dart'; import 'package:aves/theme/durations.dart'; import 'package:flushbar/flushbar.dart'; import 'package:flutter/material.dart'; @@ -27,22 +25,20 @@ mixin FeedbackMixin { // report overlay for multiple operations - OverlayEntry _opReportOverlayEntry; - - void showOpReport({ + void showOpReport({ @required BuildContext context, - @required Set selection, @required Stream opStream, - @required void Function(Set processed) onDone, + @required int itemCount, + void Function(Set processed) onDone, }) { + OverlayEntry _opReportOverlayEntry; _opReportOverlayEntry = OverlayEntry( builder: (context) => ReportOverlay( opStream: opStream, - itemCount: selection.length, + itemCount: itemCount, onDone: (processed) { - _opReportOverlayEntry?.remove(); - _opReportOverlayEntry = null; - onDone(processed); + _opReportOverlayEntry.remove(); + onDone?.call(processed); }, ), ); diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 5dfeba406..3d220401b 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -79,14 +79,14 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per if (!await checkStoragePermission(context, selection)) return; + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.delete(selection), + itemCount: selectionCount, onDone: (processed) { final deletedUris = processed.where((e) => e.success).map((e) => e.uri).toList(); final deletedCount = deletedUris.length; - final selectionCount = selection.length; if (deletedCount < selectionCount) { final count = selectionCount - deletedCount; showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); @@ -113,14 +113,14 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, MoveType.move)) return; + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.move(selection, copy: false, destinationAlbum: destinationAlbum), + itemCount: selectionCount, onDone: (processed) async { final movedOps = processed.where((e) => e.success); final movedCount = movedOps.length; - final selectionCount = selection.length; if (movedCount < selectionCount) { final count = selectionCount - movedCount; showFeedback(context, 'Failed to move ${Intl.plural(count, one: '$count item', other: '$count items')}'); diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index d201c698f..566405e0c 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -54,7 +54,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix _showRenameDialog(context, entry); break; case EntryAction.print: - EntryPrinter(entry).print(); + EntryPrinter(entry).print(context); break; case EntryAction.rotateCCW: _rotate(context, entry, clockwise: false); @@ -181,14 +181,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix selection.add(entry); } + final selectionCount = selection.length; showOpReport( context: context, - selection: selection, opStream: ImageFileService.export(selection, destinationAlbum: destinationAlbum), + itemCount: selectionCount, onDone: (processed) { final movedOps = processed.where((e) => e.success); final movedCount = movedOps.length; - final selectionCount = selection.length; if (movedCount < selectionCount) { final count = selectionCount - movedCount; showFeedback(context, 'Failed to export ${Intl.plural(count, one: '$count page', other: '$count pages')}'); diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index d774f5d38..dee300e6f 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -1,23 +1,26 @@ +import 'dart:async'; import 'dart:convert'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/metadata_service.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:flutter/widgets.dart'; import 'package:pdf/widgets.dart' as pdf; import 'package:pedantic/pedantic.dart'; import 'package:printing/printing.dart'; -class EntryPrinter { +class EntryPrinter with FeedbackMixin { final AvesEntry entry; - const EntryPrinter(this.entry); + EntryPrinter(this.entry); - Future print() async { + Future print(BuildContext context) async { final documentName = entry.bestTitle ?? 'Aves'; final doc = pdf.Document(title: documentName); - final pages = await _buildPages(); + final pages = await _buildPages(context); if (pages.isNotEmpty) { pages.forEach(doc.addPage); // Page unawaited(Printing.layoutPdf( @@ -27,7 +30,7 @@ class EntryPrinter { } } - Future> _buildPages() async { + Future> _buildPages(BuildContext context) async { final pages = []; void _addPdfPage(pdf.Widget pdfChild) { @@ -47,10 +50,18 @@ class EntryPrinter { if (entry.isMultipage) { final multiPageInfo = await MetadataService.getMultiPageInfo(entry); if (multiPageInfo.pageCount > 1) { + final streamController = StreamController.broadcast(); + showOpReport( + context: context, + opStream: streamController.stream, + itemCount: multiPageInfo.pageCount, + ); for (final page in multiPageInfo.pages) { final pageEntry = entry.getPageEntry(page); _addPdfPage(await _buildPageImage(pageEntry)); + streamController.sink.add(pageEntry); } + await streamController.close(); } } if (pages.isEmpty) { From 2d893d441572d59e97512678bc415c05b9ab83fa Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 28 Jan 2021 16:31:37 +0900 Subject: [PATCH 34/44] safer platform calls --- .../aves/channel/calls/AppAdapterHandler.kt | 7 ++-- .../thibault/aves/channel/calls/Coresult.kt | 22 +++++++++++ .../aves/channel/calls/DebugHandler.kt | 13 ++++--- .../aves/channel/calls/ImageFileHandler.kt | 26 +++++++------ .../aves/channel/calls/MetadataHandler.kt | 17 +++++---- .../aves/channel/calls/StorageHandler.kt | 38 +++++++++---------- 6 files changed, 76 insertions(+), 47 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 6930273bc..3e2ebda05 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -12,6 +12,7 @@ import androidx.core.content.FileProvider import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.request.RequestOptions +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.MethodCall @@ -28,8 +29,8 @@ import kotlin.math.roundToInt class AppAdapterHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { getAppIcon(call, Coresult(result)) } - "getAppNames" -> GlobalScope.launch(Dispatchers.IO) { getAppNames(Coresult(result)) } + "getAppNames" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppNames) } + "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppIcon) } "edit" -> { val title = call.argument("title") val uri = call.argument("uri")?.let { Uri.parse(it) } @@ -61,7 +62,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { } } - private fun getAppNames(result: MethodChannel.Result) { + private fun getAppNames(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { val nameMap = HashMap() val intent = Intent(Intent.ACTION_MAIN, null) .addCategory(Intent.CATEGORY_LAUNCHER) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt index d2db62561..5207bde7d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt @@ -1,9 +1,11 @@ package deckers.thibault.aves.channel.calls +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlin.reflect.KSuspendFunction2 // ensure `result` methods are called on the main looper thread class Coresult internal constructor(private val methodResult: MethodChannel.Result) : MethodChannel.Result { @@ -20,4 +22,24 @@ class Coresult internal constructor(private val methodResult: MethodChannel.Resu override fun notImplemented() { mainScope.launch { methodResult.notImplemented() } } + + companion object { + fun safe(call: MethodCall, result: MethodChannel.Result, function: (call: MethodCall, result: MethodChannel.Result) -> Unit) { + val res = Coresult(result) + try { + function(call, res) + } catch (e: Exception) { + res.error("safe-exception", e.message, e.stackTraceToString()) + } + } + + suspend fun safesus(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2) { + val res = Coresult(result) + try { + function(call, res) + } catch (e: Exception) { + res.error("safe-exception", e.message, e.stackTraceToString()) + } + } + } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 22e4c84ca..13ec93da6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -12,6 +12,7 @@ import android.util.Log import androidx.exifinterface.media.ExifInterface import com.drew.imaging.ImageMetadataReader import com.drew.metadata.file.FileTypeDirectory +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.metadata.ExifInterfaceHelper import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper import deckers.thibault.aves.metadata.Metadata @@ -37,12 +38,12 @@ class DebugHandler(private val context: Context) : MethodCallHandler { when (call.method) { "getContextDirs" -> result.success(getContextDirs()) "getEnv" -> result.success(System.getenv()) - "getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { getBitmapFactoryInfo(call, Coresult(result)) } - "getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { getContentResolverMetadata(call, Coresult(result)) } - "getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { getExifInterfaceMetadata(call, Coresult(result)) } - "getMediaMetadataRetrieverMetadata" -> GlobalScope.launch(Dispatchers.IO) { getMediaMetadataRetrieverMetadata(call, Coresult(result)) } - "getMetadataExtractorSummary" -> GlobalScope.launch(Dispatchers.IO) { getMetadataExtractorSummary(call, Coresult(result)) } - "getTiffStructure" -> GlobalScope.launch(Dispatchers.IO) { getTiffStructure(call, Coresult(result)) } + "getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getBitmapFactoryInfo) } + "getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverMetadata) } + "getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifInterfaceMetadata) } + "getMediaMetadataRetrieverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMediaMetadataRetrieverMetadata) } + "getMetadataExtractorSummary" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMetadataExtractorSummary) } + "getTiffStructure" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getTiffStructure) } else -> result.notImplemented() } } 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 f37b4f2ce..776747900 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 @@ -5,6 +5,8 @@ import android.graphics.Rect import android.net.Uri import android.util.Size import com.bumptech.glide.Glide +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher @@ -29,17 +31,14 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getObsoleteEntries" -> GlobalScope.launch(Dispatchers.IO) { getObsoleteEntries(call, Coresult(result)) } - "getEntry" -> GlobalScope.launch(Dispatchers.IO) { getEntry(call, Coresult(result)) } - "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { getThumbnail(call, Coresult(result)) } - "getRegion" -> GlobalScope.launch(Dispatchers.IO) { getRegion(call, Coresult(result)) } - "clearSizedThumbnailDiskCache" -> { - GlobalScope.launch(Dispatchers.IO) { Glide.get(activity).clearDiskCache() } - result.success(null) - } - "rename" -> GlobalScope.launch(Dispatchers.IO) { rename(call, Coresult(result)) } - "rotate" -> GlobalScope.launch(Dispatchers.IO) { rotate(call, Coresult(result)) } - "flip" -> GlobalScope.launch(Dispatchers.IO) { flip(call, Coresult(result)) } + "getObsoleteEntries" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getObsoleteEntries) } + "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getEntry) } + "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getThumbnail) } + "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getRegion) } + "clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) } + "rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) } + "rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) } + "flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) } else -> result.notImplemented() } } @@ -143,6 +142,11 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { }) } + private fun clearSizedThumbnailDiskCache(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + Glide.get(activity).clearDiskCache() + result.success(null) + } + private suspend fun rename(call: MethodCall, result: MethodChannel.Result) { val entryMap = call.argument("entry") val newName = call.argument("newName") diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 689d79458..57fdb11b2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -20,6 +20,7 @@ import com.drew.metadata.iptc.IptcDirectory import com.drew.metadata.mp4.media.Mp4UuidBoxDirectory import com.drew.metadata.webp.WebpDirectory import com.drew.metadata.xmp.XmpDirectory +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.metadata.* import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis @@ -69,14 +70,14 @@ import kotlin.math.roundToLong class MetadataHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { getAllMetadata(call, Coresult(result)) } - "getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { getCatalogMetadata(call, Coresult(result)) } - "getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { getOverlayMetadata(call, Coresult(result)) } - "getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { getMultiPageInfo(call, Coresult(result)) } - "getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { getPanoramaInfo(call, Coresult(result)) } - "getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { getEmbeddedPictures(call, Coresult(result)) } - "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { getExifThumbnails(call, Coresult(result)) } - "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { extractXmpDataProp(call, Coresult(result)) } + "getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAllMetadata) } + "getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getCatalogMetadata) } + "getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getOverlayMetadata) } + "getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) } + "getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) } + "getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEmbeddedPictures) } + "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) } + "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) } else -> result.notImplemented() } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt index 6ff8babbb..248db4ff6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt @@ -5,7 +5,7 @@ import android.media.MediaScannerConnection import android.net.Uri import android.os.Build import android.os.storage.StorageManager -import androidx.annotation.RequiresApi +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.utils.PermissionManager import deckers.thibault.aves.utils.StorageUtils.getVolumePaths import io.flutter.plugin.common.MethodCall @@ -20,27 +20,18 @@ import java.util.* class StorageHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getStorageVolumes" -> { - val volumes: List> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - storageVolumes - } else { - // TODO TLAD find alternative for Android getFreeSpace(call, result) - "getGrantedDirectories" -> result.success(ArrayList(PermissionManager.getGrantedDirs(context))) - "getInaccessibleDirectories" -> getInaccessibleDirectories(call, result) - "revokeDirectoryAccess" -> revokeDirectoryAccess(call, result) - "scanFile" -> GlobalScope.launch(Dispatchers.IO) { scanFile(call, Coresult(result)) } + "getStorageVolumes" -> safe(call, result, ::getStorageVolumes) + "getFreeSpace" -> safe(call, result, ::getFreeSpace) + "getGrantedDirectories" -> safe(call, result, ::getGrantedDirectories) + "getInaccessibleDirectories" -> safe(call, result, ::getInaccessibleDirectories) + "revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess) + "scanFile" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::scanFile) } else -> result.notImplemented() } } - private val storageVolumes: List> - @RequiresApi(api = Build.VERSION_CODES.N) - get() { + private fun getStorageVolumes(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + val volumes: List> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val volumes = ArrayList>() val sm = context.getSystemService(StorageManager::class.java) if (sm != null) { @@ -61,8 +52,13 @@ class StorageHandler(private val context: Context) : MethodCallHandler { } } } - return volumes + volumes + } else { + // TODO TLAD find alternative for Android ("path") @@ -93,6 +89,10 @@ class StorageHandler(private val context: Context) : MethodCallHandler { } } + private fun getGrantedDirectories(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + result.success(ArrayList(PermissionManager.getGrantedDirs(context))) + } + private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) { val dirPaths = call.argument>("dirPaths") if (dirPaths == null) { From 24dcb5b021382c83cd747349f23dfb47351bb7f1 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 28 Jan 2021 17:16:06 +0900 Subject: [PATCH 35/44] info: show owner app, if any --- .../aves/channel/calls/MetadataHandler.kt | 55 +++++++++++++++ lib/services/metadata_service.dart | 13 ++++ lib/widgets/viewer/info/basic_section.dart | 69 ++++++++++++++++++- lib/widgets/viewer/info/common.dart | 24 +++---- 4 files changed, 148 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 57fdb11b2..a3f64479d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -1,10 +1,15 @@ package deckers.thibault.aves.channel.calls +import android.content.ContentResolver +import android.content.ContentUris import android.content.Context +import android.database.Cursor import android.media.MediaExtractor import android.media.MediaFormat import android.media.MediaMetadataRetriever import android.net.Uri +import android.os.Build +import android.provider.MediaStore import android.util.Log import androidx.exifinterface.media.ExifInterface import com.adobe.internal.xmp.XMPException @@ -75,6 +80,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { "getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getOverlayMetadata) } "getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) } "getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) } + "getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) } "getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEmbeddedPictures) } "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) } "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) } @@ -622,6 +628,55 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { result.error("getPanoramaInfo-empty", "failed to read XMP from uri=$uri", null) } + private fun getContentResolverProp(call: MethodCall, result: MethodChannel.Result) { + val mimeType = call.argument("mimeType") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val prop = call.argument("prop") + if (mimeType == null || uri == null || prop == null) { + result.error("getContentResolverProp-args", "failed because of missing arguments", null) + return + } + + var contentUri: Uri = uri + if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) { + try { + val id = ContentUris.parseId(uri) + contentUri = when { + isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) + isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) + else -> uri + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + contentUri = MediaStore.setRequireOriginal(contentUri) + } + } catch (e: NumberFormatException) { + // ignore + } + } + + val projection = arrayOf(prop) + val cursor = context.contentResolver.query(contentUri, projection, null, null, null) + if (cursor != null && cursor.moveToFirst()) { + var value: Any? = null + try { + value = when (cursor.getType(0)) { + Cursor.FIELD_TYPE_NULL -> null + Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0) + Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0) + Cursor.FIELD_TYPE_STRING -> cursor.getString(0) + Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0) + else -> null + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get value for key=$prop", e) + } + cursor.close() + result.success(value?.toString()) + } else { + result.error("getContentResolverProp-null", "failed to get cursor for contentUri=$contentUri", null) + } + } + private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index e4a2e0b90..d55799255 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -113,6 +113,19 @@ class MetadataService { return null; } + static Future getContentResolverProp(AvesEntry entry, String prop) async { + try { + return await platform.invokeMethod('getContentResolverProp', { + 'mimeType': entry.mimeType, + 'uri': entry.uri, + 'prop': prop, + }); + } on PlatformException catch (e) { + debugPrint('getContentResolverProp failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + return null; + } + static Future> getEmbeddedPictures(String uri) async { try { final result = await platform.invokeMethod('getEmbeddedPictures', { diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 26cedfa6a..3e3cf4931 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -1,11 +1,14 @@ +import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/ref/mime_types.dart'; +import 'package:aves/services/metadata_service.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -55,6 +58,7 @@ class BasicSection extends StatelessWidget { 'URI': uri, if (path != null) 'Path': path, }), + OwnerProp(entry: entry), _buildChips(), ], ); @@ -102,3 +106,66 @@ class BasicSection extends StatelessWidget { }; } } + +class OwnerProp extends StatefulWidget { + final AvesEntry entry; + + const OwnerProp({ + @required this.entry, + }); + + @override + _OwnerPropState createState() => _OwnerPropState(); +} + +class _OwnerPropState extends State { + Future _loader; + + static const iconSize = 20.0; + + @override + void initState() { + super.initState(); + _loader = MetadataService.getContentResolverProp(widget.entry, 'owner_package_name'); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _loader, + builder: (context, snapshot) { + final packageName = snapshot.data; + if (packageName == null) return SizedBox(); + final appName = androidFileUtils.appNameMap.entries.firstWhere((kv) => kv.value == packageName, orElse: () => null)?.key ?? packageName; + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Owned by', + style: InfoRowGroup.keyStyle, + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Image( + image: AppIconImage( + packageName: packageName, + size: iconSize, + ), + width: iconSize, + height: iconSize, + ), + ), + ), + TextSpan( + text: appName, + style: InfoRowGroup.baseStyle, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index 4002d1571..5b0ff8243 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -42,6 +42,12 @@ class InfoRowGroup extends StatefulWidget { final int maxValueLength; final Map linkHandlers; + static const keyValuePadding = 16; + static const linkColor = Colors.blue; + static final baseStyle = TextStyle(fontFamily: 'Concourse'); + static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 1.7); + static final linkStyle = baseStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); + const InfoRowGroup( this.keyValues, { this.maxValueLength = 0, @@ -61,20 +67,14 @@ class _InfoRowGroupState extends State { Map get linkHandlers => widget.linkHandlers; - static const keyValuePadding = 16; - static const linkColor = Colors.blue; - static final baseStyle = TextStyle(fontFamily: 'Concourse'); - static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 1.7); - static final linkStyle = baseStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); - @override Widget build(BuildContext context) { if (keyValues.isEmpty) return SizedBox.shrink(); // compute the size of keys and space in order to align values final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: '$key', style: keyStyle), textScaleFactor)))); - final baseSpaceWidth = _getSpanWidth(TextSpan(text: '\u200A' * 100, style: baseStyle), textScaleFactor); + final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: '$key', style: InfoRowGroup.keyStyle), textScaleFactor)))); + final baseSpaceWidth = _getSpanWidth(TextSpan(text: '\u200A' * 100, style: InfoRowGroup.baseStyle), textScaleFactor); final lastKey = keyValues.keys.last; return LayoutBuilder( @@ -100,7 +100,7 @@ class _InfoRowGroupState extends State { value = handler.linkText; // open link on tap recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context); - style = linkStyle; + style = InfoRowGroup.linkStyle; } else { value = kv.value; // long values are clipped, and made expandable by tapping them @@ -118,18 +118,18 @@ class _InfoRowGroupState extends State { // as of Flutter v1.22.4, `SelectableText` cannot contain `WidgetSpan` // so we add padding using multiple hair spaces instead - final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + keyValuePadding; + final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + InfoRowGroup.keyValuePadding; final spaceCount = (100 * thisSpaceSize / baseSpaceWidth).round(); return [ - TextSpan(text: key, style: keyStyle), + TextSpan(text: key, style: InfoRowGroup.keyStyle), TextSpan(text: '\u200A' * spaceCount), TextSpan(text: value, style: style, recognizer: recognizer), ]; }, ).toList(), ), - style: baseStyle, + style: InfoRowGroup.baseStyle, ), ], ); From 8e44d4a9d959a5bf92b1b89de11e70943b937485 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 29 Jan 2021 11:23:34 +0900 Subject: [PATCH 36/44] debug: package list --- lib/utils/android_file_utils.dart | 15 ++-- lib/widgets/debug/android_apps.dart | 80 ++++++++++++++++++++++ lib/widgets/debug/android_dirs.dart | 2 +- lib/widgets/debug/app_debug_page.dart | 2 + lib/widgets/viewer/info/basic_section.dart | 2 +- lib/widgets/viewer/info/info_page.dart | 2 +- 6 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 lib/widgets/debug/android_apps.dart diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index eec38cddf..7565db251 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -8,7 +8,7 @@ final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath; Set storageVolumes = {}; - Map appNameMap = {}; + Map _installedAppNameMap = {}; AChangeNotifier appNameChangeNotifier = AChangeNotifier(); @@ -25,7 +25,7 @@ class AndroidFileUtils { } Future initAppNames() async { - appNameMap = await AndroidAppService.getAppNames() + _installedAppNameMap = await AndroidAppService.getAppNames() ..addAll({'KakaoTalkDownload': 'com.kakao.talk'}); appNameChangeNotifier.notifyListeners(); } @@ -50,15 +50,16 @@ class AndroidFileUtils { if (isScreenshotsPath(albumDirectory)) return AlbumType.screenshots; final parts = albumDirectory.split(separator); - if (albumDirectory.startsWith(primaryStorage) && appNameMap.keys.contains(parts.last)) return AlbumType.app; + if (albumDirectory.startsWith(primaryStorage) && _isInstalledAppName(parts.last)) return AlbumType.app; } return AlbumType.regular; } - String getAlbumAppPackageName(String albumDirectory) { - final parts = albumDirectory.split(separator); - return appNameMap[parts.last]; - } + bool _isInstalledAppName(String name) => _installedAppNameMap.keys.contains(name); + + String getAlbumAppPackageName(String albumDirectory) => _installedAppNameMap[albumDirectory.split(separator).last]; + + String getAppName(String packageName) => _installedAppNameMap.entries.firstWhere((kv) => kv.value == packageName, orElse: () => null)?.key; } enum AlbumType { regular, app, camera, download, screenRecordings, screenshots } diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart new file mode 100644 index 000000000..e929e34eb --- /dev/null +++ b/lib/widgets/debug/android_apps.dart @@ -0,0 +1,80 @@ +import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/services/android_app_service.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class DebugAndroidAppSection extends StatefulWidget { + @override + _DebugAndroidAppSectionState createState() => _DebugAndroidAppSectionState(); +} + +class _DebugAndroidAppSectionState extends State with AutomaticKeepAliveClientMixin { + Future _loader; + + static const iconSize = 20.0; + + @override + void initState() { + super.initState(); + _loader = AndroidAppService.getAppNames(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + return AvesExpansionTile( + title: 'Android Apps', + children: [ + Padding( + padding: 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 entries = snapshot.data.entries.toList()..sort((kv1, kv2) => compareAsciiUpperCase(kv1.value, kv2.value)); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: entries.map((kv) { + final appName = kv.key.toString(); + final packageName = kv.value.toString(); + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image( + image: AppIconImage( + packageName: packageName, + size: iconSize, + ), + width: iconSize, + height: iconSize, + ), + ), + TextSpan( + text: ' $packageName', + style: InfoRowGroup.keyStyle, + ), + TextSpan( + text: ' $appName', + style: InfoRowGroup.baseStyle, + ), + ], + ), + ); + }).toList(), + ); + }, + ), + ), + ], + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/debug/android_dirs.dart b/lib/widgets/debug/android_dirs.dart index 3731170fe..92e71bdf7 100644 --- a/lib/widgets/debug/android_dirs.dart +++ b/lib/widgets/debug/android_dirs.dart @@ -24,7 +24,7 @@ class _DebugAndroidDirSectionState extends State with Au super.build(context); return AvesExpansionTile( - title: 'Android Dir', + title: 'Android Dirs', children: [ Padding( padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 4d560ceb8..b600f0380 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/debug/android_apps.dart'; import 'package:aves/widgets/debug/android_dirs.dart'; import 'package:aves/widgets/debug/android_env.dart'; import 'package:aves/widgets/debug/cache.dart'; @@ -42,6 +43,7 @@ class _AppDebugPageState extends State { padding: EdgeInsets.all(8), children: [ _buildGeneralTabView(), + DebugAndroidAppSection(), DebugAndroidDirSection(), DebugAndroidEnvironmentSection(), DebugCacheSection(), diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 3e3cf4931..d2ba17492 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -136,7 +136,7 @@ class _OwnerPropState extends State { builder: (context, snapshot) { final packageName = snapshot.data; if (packageName == null) return SizedBox(); - final appName = androidFileUtils.appNameMap.entries.firstWhere((kv) => kv.value == packageName, orElse: () => null)?.key ?? packageName; + final appName = androidFileUtils.getAppName(packageName) ?? packageName; return Text.rich( TextSpan( children: [ diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index b84e2a4d7..3adede21f 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -64,7 +64,7 @@ class _InfoPageState extends State { entry: entry, visibleNotifier: widget.visibleNotifier, scrollController: _scrollController, - split: mqWidth > 400, + split: mqWidth > 600, goToViewer: _goToViewer, ) : SizedBox.shrink(); From f133ebf624365eb095fc64c8d6894efa5357a657 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 30 Jan 2021 12:38:35 +0900 Subject: [PATCH 37/44] improved package retrieval --- android/app/src/main/AndroidManifest.xml | 1 - .../aves/channel/calls/AppAdapterHandler.kt | 76 ++++++++++--------- .../aves/channel/calls/DebugHandler.kt | 2 +- .../aves/channel/calls/ImageFileHandler.kt | 2 +- .../aves/channel/calls/MetadataHandler.kt | 2 +- .../channel/streams/ImageOpStreamHandler.kt | 2 +- .../streams/MediaStoreStreamHandler.kt | 2 +- .../deckers/thibault/aves/model/AvesEntry.kt | 2 +- .../deckers/thibault/aves/model/FieldMap.kt | 3 + .../thibault/aves/model/SourceEntry.kt | 2 +- .../aves/model/provider/ImageProvider.kt | 3 +- .../model/provider/MediaStoreImageProvider.kt | 1 + lib/services/android_app_service.dart | 15 +++- lib/services/android_file_service.dart | 6 +- lib/utils/android_file_utils.dart | 75 ++++++++++++++---- lib/widgets/debug/android_apps.dart | 41 +++++++--- lib/widgets/viewer/info/basic_section.dart | 2 +- 17 files changed, 158 insertions(+), 79 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1faac6cc8..66f30eaf6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -40,7 +40,6 @@ - diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 3e2ebda05..c4b8e3377 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -13,6 +13,7 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.request.RequestOptions import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.MethodCall @@ -29,7 +30,7 @@ import kotlin.math.roundToInt class AppAdapterHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getAppNames" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppNames) } + "getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) } "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppIcon) } "edit" -> { val title = call.argument("title") @@ -62,46 +63,51 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { } } - private fun getAppNames(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { - val nameMap = HashMap() - val intent = Intent(Intent.ACTION_MAIN, null) - .addCategory(Intent.CATEGORY_LAUNCHER) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + private fun getPackages(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + val packages = HashMap() - // apps tend to use their name in English when creating folders - // so we get their names in English as well as the current locale - val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) } + fun addPackageDetails(intent: Intent) { + // apps tend to use their name in English when creating folders + // so we get their names in English as well as the current locale + val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) } - val pm = context.packageManager - for (resolveInfo in pm.queryIntentActivities(intent, 0)) { - val ai = resolveInfo.activityInfo.applicationInfo - val isSystemPackage = ai.flags and ApplicationInfo.FLAG_SYSTEM != 0 - if (!isSystemPackage) { - val packageName = ai.packageName - - val currentLabel = pm.getApplicationLabel(ai).toString() - nameMap[currentLabel] = packageName - - val labelRes = ai.labelRes - if (labelRes != 0) { - try { - val resources = pm.getResourcesForApplication(ai) - // `updateConfiguration` is deprecated but it seems to be the only way - // to query resources from another app with a specific locale. - // The following methods do not work: - // - `resources.getConfiguration().setLocale(...)` - // - getting a package manager from a custom context with `context.createConfigurationContext(config)` - @Suppress("DEPRECATION") - resources.updateConfiguration(englishConfig, resources.displayMetrics) - val englishLabel = resources.getString(labelRes) - nameMap[englishLabel] = packageName - } catch (e: PackageManager.NameNotFoundException) { - Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e) + val pm = context.packageManager + for (resolveInfo in pm.queryIntentActivities(intent, 0)) { + val appInfo = resolveInfo.activityInfo.applicationInfo + val packageName = appInfo.packageName + if (!packages.containsKey(packageName)) { + val currentLabel = pm.getApplicationLabel(appInfo).toString() + val englishLabel: String? = appInfo.labelRes.takeIf { it != 0 }?.let { labelRes -> + var englishLabel: String? = null + try { + val resources = pm.getResourcesForApplication(appInfo) + // `updateConfiguration` is deprecated but it seems to be the only way + // to query resources from another app with a specific locale. + // The following methods do not work: + // - `resources.getConfiguration().setLocale(...)` + // - getting a package manager from a custom context with `context.createConfigurationContext(config)` + @Suppress("DEPRECATION") + resources.updateConfiguration(englishConfig, resources.displayMetrics) + englishLabel = resources.getString(labelRes) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e) + } + englishLabel } + packages[packageName] = hashMapOf( + "packageName" to packageName, + "categoryLauncher" to intent.hasCategory(Intent.CATEGORY_LAUNCHER), + "isSystem" to (appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0), + "currentLabel" to currentLabel, + "englishLabel" to englishLabel, + ) } } } - result.success(nameMap) + + addPackageDetails(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)) + addPackageDetails(Intent(Intent.ACTION_MAIN)) + result.success(ArrayList(packages.values)) } private fun getAppIcon(call: MethodCall, result: MethodChannel.Result) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 13ec93da6..6b4401073 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -16,7 +16,7 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.metadata.ExifInterfaceHelper import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper import deckers.thibault.aves.metadata.Metadata -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface 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 776747900..a9c831565 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 @@ -11,7 +11,7 @@ import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher import deckers.thibault.aves.model.ExifOrientationOp -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.model.provider.MediaStoreImageProvider diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index a3f64479d..940ab0cba 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -46,7 +46,7 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff import deckers.thibault.aves.metadata.XMP.getSafeDateMillis import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText import deckers.thibault.aves.metadata.XMP.isPanorama -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.provider.FileImageProvider import deckers.thibault.aves.model.provider.ImageProvider import deckers.thibault.aves.utils.BitmapUtils diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index b70de372a..86e8d0650 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -6,7 +6,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import deckers.thibault.aves.model.AvesEntry -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.utils.LogUtils diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt index f2892cfb2..ac9fec726 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt @@ -4,7 +4,7 @@ import android.content.Context import android.os.Handler import android.os.Looper import android.util.Log -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.provider.MediaStoreImageProvider import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.EventChannel diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt index dfa0cbd32..872ed6819 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt @@ -1,7 +1,7 @@ package deckers.thibault.aves.model import android.net.Uri -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap class AvesEntry(map: FieldMap) { val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt new file mode 100644 index 000000000..78592a2b0 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt @@ -0,0 +1,3 @@ +package deckers.thibault.aves.model + +typealias FieldMap = MutableMap diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt index a482087ac..5a51905da 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt @@ -25,7 +25,7 @@ import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong -import deckers.thibault.aves.model.provider.FieldMap +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils import org.beyka.tiffbitmapfactory.TiffBitmapFactory diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index 4ff6a1ff8..f92ba728c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -18,6 +18,7 @@ import deckers.thibault.aves.decoder.MultiTrackImage import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.ExifOrientationOp +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes @@ -348,5 +349,3 @@ abstract class ImageProvider { private val LOG_TAG = LogUtils.createTag(ImageProvider::class.java) } } - -typealias FieldMap = MutableMap diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index f06b5ea04..b87c55f49 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -9,6 +9,7 @@ import android.provider.MediaStore import android.util.Log import com.commonsware.cwac.document.DocumentFileCompat import deckers.thibault.aves.model.AvesEntry +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 4f6b6750c..9a379334d 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:aves/model/entry.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -8,12 +9,18 @@ import 'package:flutter/services.dart'; class AndroidAppService { static const platform = MethodChannel('deckers.thibault/aves/app'); - static Future getAppNames() async { + static Future> getPackages() async { try { - final result = await platform.invokeMethod('getAppNames'); - return result as Map; + 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); + if (kakaoTalk != null) { + kakaoTalk.ownedDirs.add('KakaoTalkDownload'); + } + return packages; } on PlatformException catch (e) { - debugPrint('getAppNames failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); + debugPrint('getPackages failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } return {}; } diff --git a/lib/services/android_file_service.dart b/lib/services/android_file_service.dart index 0a0bb2f12..7b8668315 100644 --- a/lib/services/android_file_service.dart +++ b/lib/services/android_file_service.dart @@ -9,14 +9,14 @@ class AndroidFileService { static const platform = MethodChannel('deckers.thibault/aves/storage'); static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storageaccessstream'); - static Future> getStorageVolumes() async { + static Future> getStorageVolumes() async { try { final result = await platform.invokeMethod('getStorageVolumes'); - return (result as List).cast(); + return (result as List).cast().map((map) => StorageVolume.fromMap(map)).toSet(); } on PlatformException catch (e) { debugPrint('getStorageVolumes failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } - return []; + return {}; } static Future getFreeSpace(StorageVolume volume) async { diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index 7565db251..949e3424a 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -1,6 +1,7 @@ import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/change_notifier.dart'; +import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); @@ -8,14 +9,17 @@ final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath; Set storageVolumes = {}; - Map _installedAppNameMap = {}; + Set _packages = {}; + List _potentialAppDirs = []; AChangeNotifier appNameChangeNotifier = AChangeNotifier(); + Iterable get _launcherPackages => _packages.where((package) => package.categoryLauncher); + AndroidFileUtils._private(); Future init() async { - storageVolumes = (await AndroidFileService.getStorageVolumes()).map((map) => StorageVolume.fromMap(map)).toSet(); + storageVolumes = await AndroidFileService.getStorageVolumes(); // path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files' primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path; dcimPath = join(primaryStorage, 'DCIM'); @@ -25,8 +29,8 @@ class AndroidFileUtils { } Future initAppNames() async { - _installedAppNameMap = await AndroidAppService.getAppNames() - ..addAll({'KakaoTalkDownload': 'com.kakao.talk'}); + _packages = await AndroidAppService.getPackages(); + _potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList(); appNameChangeNotifier.notifyListeners(); } @@ -42,28 +46,67 @@ class AndroidFileUtils { bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; - AlbumType getAlbumType(String albumDirectory) { - if (albumDirectory != null) { - if (isCameraPath(albumDirectory)) return AlbumType.camera; - if (isDownloadPath(albumDirectory)) return AlbumType.download; - if (isScreenRecordingsPath(albumDirectory)) return AlbumType.screenRecordings; - if (isScreenshotsPath(albumDirectory)) return AlbumType.screenshots; + 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; - final parts = albumDirectory.split(separator); - if (albumDirectory.startsWith(primaryStorage) && _isInstalledAppName(parts.last)) return AlbumType.app; + final dir = albumPath.split(separator).last; + if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; } return AlbumType.regular; } - bool _isInstalledAppName(String name) => _installedAppNameMap.keys.contains(name); + String getAlbumAppPackageName(String albumPath) { + if (albumPath == null) return null; + final dir = albumPath.split(separator).last; + final package = _launcherPackages.firstWhere((package) => package.potentialDirs.contains(dir), orElse: () => null); + return package?.packageName; + } - String getAlbumAppPackageName(String albumDirectory) => _installedAppNameMap[albumDirectory.split(separator).last]; - - String getAppName(String packageName) => _installedAppNameMap.entries.firstWhere((kv) => kv.value == packageName, orElse: () => null)?.key; + String getCurrentAppName(String packageName) { + final package = _packages.firstWhere((package) => package.packageName == packageName, orElse: () => null); + return package?.currentLabel; + } } enum AlbumType { regular, app, camera, download, screenRecordings, screenshots } +class Package { + final String packageName, currentLabel, englishLabel; + final bool categoryLauncher, isSystem; + final Set ownedDirs = {}; + + Package({ + this.packageName, + this.currentLabel, + this.englishLabel, + this.categoryLauncher, + this.isSystem, + }); + + factory Package.fromMap(Map map) { + return Package( + packageName: map['packageName'], + currentLabel: map['currentLabel'], + englishLabel: map['englishLabel'], + categoryLauncher: map['categoryLauncher'], + isSystem: map['isSystem'], + ); + } + + Set get potentialDirs => [ + currentLabel, + englishLabel, + ...ownedDirs, + ].where((dir) => dir != null).toSet(); + + @override + String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}'; +} + class StorageVolume { final String description, path, state; final bool isEmulated, isPrimary, isRemovable; diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart index e929e34eb..9854088c1 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/android_apps.dart @@ -1,5 +1,6 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/services/android_app_service.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:collection/collection.dart'; @@ -11,14 +12,14 @@ class DebugAndroidAppSection extends StatefulWidget { } class _DebugAndroidAppSectionState extends State with AutomaticKeepAliveClientMixin { - Future _loader; + Future> _loader; static const iconSize = 20.0; @override void initState() { super.initState(); - _loader = AndroidAppService.getAppNames(); + _loader = AndroidAppService.getPackages(); } @override @@ -30,17 +31,17 @@ class _DebugAndroidAppSectionState extends State with Au children: [ Padding( padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: FutureBuilder( + child: FutureBuilder>( future: _loader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final entries = snapshot.data.entries.toList()..sort((kv1, kv2) => compareAsciiUpperCase(kv1.value, kv2.value)); + 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)); return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: entries.map((kv) { - final appName = kv.key.toString(); - final packageName = kv.value.toString(); + children: packages.map((package) { return Text.rich( TextSpan( children: [ @@ -48,7 +49,7 @@ class _DebugAndroidAppSectionState extends State with Au alignment: PlaceholderAlignment.middle, child: Image( image: AppIconImage( - packageName: packageName, + packageName: package.packageName, size: iconSize, ), width: iconSize, @@ -56,11 +57,31 @@ class _DebugAndroidAppSectionState extends State with Au ), ), TextSpan( - text: ' $packageName', + text: ' ${package.packageName}\n', style: InfoRowGroup.keyStyle, ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: IconTheme( + data: package.categoryLauncher ? enabledTheme : disabledTheme, + child: Icon( + Icons.launch_outlined, + size: iconSize, + ), + ), + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: IconTheme( + data: package.isSystem ? enabledTheme : disabledTheme, + child: Icon( + Icons.android, + size: iconSize, + ), + ), + ), TextSpan( - text: ' $appName', + text: ' ${package.potentialDirs.join(', ')}\n', style: InfoRowGroup.baseStyle, ), ], diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index d2ba17492..b900b4b7c 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -136,7 +136,7 @@ class _OwnerPropState extends State { builder: (context, snapshot) { final packageName = snapshot.data; if (packageName == null) return SizedBox(); - final appName = androidFileUtils.getAppName(packageName) ?? packageName; + final appName = androidFileUtils.getCurrentAppName(packageName) ?? packageName; return Text.rich( TextSpan( children: [ From 09de6bb76ba52dc33a47593723040412212fd121 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 30 Jan 2021 13:00:32 +0900 Subject: [PATCH 38/44] info: fixed owner for current entry --- lib/widgets/viewer/info/basic_section.dart | 68 ++++++++++++++++++---- lib/widgets/viewer/info/common.dart | 2 +- lib/widgets/viewer/info/info_page.dart | 16 +++-- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index b900b4b7c..acfd8119d 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -20,12 +20,14 @@ import 'package:intl/intl.dart'; class BasicSection extends StatelessWidget { final AvesEntry entry; final CollectionLens collection; + final ValueNotifier visibleNotifier; final FilterCallback onFilter; const BasicSection({ Key key, @required this.entry, this.collection, + @required this.visibleNotifier, @required this.onFilter, }) : super(key: key); @@ -58,7 +60,10 @@ class BasicSection extends StatelessWidget { 'URI': uri, if (path != null) 'Path': path, }), - OwnerProp(entry: entry), + OwnerProp( + entry: entry, + visibleNotifier: visibleNotifier, + ), _buildChips(), ], ); @@ -109,9 +114,11 @@ class BasicSection extends StatelessWidget { class OwnerProp extends StatefulWidget { final AvesEntry entry; + final ValueNotifier visibleNotifier; const OwnerProp({ @required this.entry, + @required this.visibleNotifier, }); @override @@ -119,24 +126,53 @@ class OwnerProp extends StatefulWidget { } class _OwnerPropState extends State { - Future _loader; + final ValueNotifier _loadedUri = ValueNotifier(null); + String _ownerPackage; + + AvesEntry get entry => widget.entry; + + bool get isVisible => widget.visibleNotifier.value; static const iconSize = 20.0; @override void initState() { super.initState(); - _loader = MetadataService.getContentResolverProp(widget.entry, 'owner_package_name'); + _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); } @override Widget build(BuildContext context) { - return FutureBuilder( - future: _loader, - builder: (context, snapshot) { - final packageName = snapshot.data; - if (packageName == null) return SizedBox(); - final appName = androidFileUtils.getCurrentAppName(packageName) ?? packageName; + return ValueListenableBuilder( + valueListenable: _loadedUri, + builder: (context, uri, child) { + if (_ownerPackage == null) return SizedBox(); + final appName = androidFileUtils.getCurrentAppName(_ownerPackage) ?? _ownerPackage; + // as of Flutter v1.22.6, `SelectableText` cannot contain `WidgetSpan` + // so be use a basic `Text` instead return Text.rich( TextSpan( children: [ @@ -150,7 +186,7 @@ class _OwnerPropState extends State { padding: EdgeInsets.symmetric(horizontal: 4), child: Image( image: AppIconImage( - packageName: packageName, + packageName: _ownerPackage, size: iconSize, ), width: iconSize, @@ -168,4 +204,16 @@ class _OwnerPropState extends State { }, ); } + + Future _getOwner() async { + if (entry == null) return; + if (_loadedUri.value == entry.uri) return; + if (isVisible) { + _ownerPackage = await MetadataService.getContentResolverProp(widget.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 5b0ff8243..5d99e65f5 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -116,7 +116,7 @@ class _InfoRowGroupState extends State { value = '$value\n'; } - // as of Flutter v1.22.4, `SelectableText` cannot contain `WidgetSpan` + // 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 spaceCount = (100 * thisSpaceSize / baseSpaceWidth).round(); diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 3adede21f..112bb6310 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -156,14 +156,22 @@ class _InfoPageContentState extends State<_InfoPageContent> { 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; final locationSection = LocationSection( collection: collection, entry: entry, showTitle: !locationAtTop, - visibleNotifier: widget.visibleNotifier, + visibleNotifier: visibleNotifier, onFilter: _goToCollection, ); final basicAndLocationSliver = locationAtTop @@ -171,7 +179,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: BasicSection(entry: entry, collection: collection, onFilter: _goToCollection)), + Expanded(child: basicSection), SizedBox(width: 8), Expanded(child: locationSection), ], @@ -180,7 +188,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { : SliverList( delegate: SliverChildListDelegate.fixed( [ - BasicSection(entry: entry, collection: collection, onFilter: _goToCollection), + basicSection, locationSection, ], ), @@ -188,7 +196,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { final metadataSliver = MetadataSectionSliver( entry: entry, metadataNotifier: _metadataNotifier, - visibleNotifier: widget.visibleNotifier, + visibleNotifier: visibleNotifier, ); return CustomScrollView( From 34b6ef042857264301c30579d5372779e88a1908 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 30 Jan 2021 13:26:11 +0900 Subject: [PATCH 39/44] android: upgraded glide, google-services --- android/app/build.gradle | 4 ++-- android/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 47c92f2f2..e8cea44fa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -103,10 +103,10 @@ dependencies { implementation 'com.commonsware.cwac:document:0.4.1' implementation 'com.drewnoakes:metadata-extractor:2.15.0' implementation 'com.github.deckerst:Android-TiffBitmapFactory:f87db4305d' // forked, built by JitPack - implementation 'com.github.bumptech.glide:glide:4.11.0' + implementation 'com.github.bumptech.glide:glide:4.12.0' kapt 'androidx.annotation:annotation:1.1.0' - kapt 'com.github.bumptech.glide:compiler:4.11.0' + kapt 'com.github.bumptech.glide:compiler:4.12.0' compileOnly rootProject.findProject(':streams_channel') } diff --git a/android/build.gradle b/android/build.gradle index 65df263a7..006020e81 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,7 +9,7 @@ buildscript { // TODO TLAD upgrade AGP to 4+ when this lands on stable: https://github.com/flutter/flutter/pull/70808 classpath 'com.android.tools.build:gradle:3.6.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.4' + classpath 'com.google.gms:google-services:4.3.5' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' } } From f6434f0b5f6ddbb58b0897b9108031f941efad4e Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 30 Jan 2021 19:12:11 +0900 Subject: [PATCH 40/44] obsolete files: give error hint on viewer, allow deleting from media store --- .../model/provider/MediaStoreImageProvider.kt | 2 +- .../viewer/visual/entry_page_view.dart | 10 +++- lib/widgets/viewer/visual/error.dart | 50 +++++++++++++++---- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index b87c55f49..cf90f1b5d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -186,7 +186,7 @@ class MediaStoreImageProvider : ImageProvider() { override suspend fun delete(context: Context, uri: Uri, path: String?) { path ?: throw Exception("failed to delete file because path is null") - if (requireAccessPermission(context, path)) { + if (File(path).exists() && requireAccessPermission(context, path)) { // if the file is on SD card, calling the content resolver `delete()` removes the entry from the Media Store // but it doesn't delete the file, even if the app has the permission val df = getDocumentFile(context, path, uri) diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 30b4f3830..ea17a2478 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -129,7 +129,10 @@ class _EntryPageViewState extends State { } else if (entry.canDecode) { child = _buildRasterView(); } - child ??= ErrorView(onTap: () => onTap?.call(null)); + child ??= ErrorView( + entry: entry, + onTap: () => onTap?.call(null), + ); return widget.heroTag != null ? Hero( @@ -146,7 +149,10 @@ class _EntryPageViewState extends State { child: RasterImageView( entry: entry, viewStateNotifier: _viewStateNotifier, - errorBuilder: (context, error, stackTrace) => ErrorView(onTap: () => onTap?.call(null)), + errorBuilder: (context, error, stackTrace) => ErrorView( + entry: entry, + onTap: () => onTap?.call(null), + ), ), ); } diff --git a/lib/widgets/viewer/visual/error.dart b/lib/widgets/viewer/visual/error.dart index fbee7fb3b..f16192aed 100644 --- a/lib/widgets/viewer/visual/error.dart +++ b/lib/widgets/viewer/visual/error.dart @@ -1,26 +1,54 @@ +import 'dart:io'; + +import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class ErrorView extends StatelessWidget { +class ErrorView extends StatefulWidget { + final AvesEntry entry; final VoidCallback onTap; - const ErrorView({@required this.onTap}); + const ErrorView({ + @required this.entry, + @required this.onTap, + }); + + @override + _ErrorViewState createState() => _ErrorViewState(); +} + +class _ErrorViewState extends State { + Future _exists; + + AvesEntry get entry => widget.entry; + + @override + void initState() { + super.initState(); + _exists = entry.path != null ? File(entry.path).exists() : SynchronousFuture(true); + } @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => onTap?.call(), - // use a `Container` with a dummy color to make it expand - // so that we can also detect taps around the title `Text` + onTap: () => widget.onTap?.call(), + // use container to expand constraints, so that the user can tap anywhere child: Container( - color: Colors.transparent, - child: EmptyContent( - icon: AIcons.error, - text: 'Oops!', - alignment: Alignment.center, - ), + // opaque to cover potential lower quality layer below + color: Colors.black, + child: FutureBuilder( + future: _exists, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) return SizedBox(); + final exists = snapshot.data; + return EmptyContent( + icon: AIcons.error, + text: exists ? 'Oops!' : 'The file no longer exists.', + alignment: Alignment.center, + ); + }), ), ); } From f2de483df85d76c928f4f2fc1211a0968f2a0a5d Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 30 Jan 2021 20:57:53 +0900 Subject: [PATCH 41/44] packages upgrade --- pubspec.lock | 72 ++++++---------------------------------------------- 1 file changed, 8 insertions(+), 64 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 97fe07ccf..514b21954 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,13 +57,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.1" - cached_network_image: - dependency: transitive - description: - name: cached_network_image - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0" characters: dependency: transitive description: @@ -288,20 +281,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_blurhash: - dependency: transitive - description: - name: flutter_blurhash - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" flutter_cube: dependency: transitive description: @@ -326,7 +305,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: d5a827929882f06a30cb82de29feec545d8f00da + resolved-ref: c1b7f25e2a3bc67ab7b30561af49a62ae9a8c409 url: "git://github.com/deckerst/flutter_ijkplayer.git" source: git version: "0.3.7" @@ -343,7 +322,7 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "0.10.1+1" + version: "0.11.0" flutter_markdown: dependency: "direct main" description: @@ -583,13 +562,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.13" - octo_image: - dependency: transitive - description: - name: octo_image - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" overlay_support: dependency: "direct main" description: @@ -610,7 +582,7 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.3+2" + version: "0.4.3+4" palette_generator: dependency: "direct main" description: @@ -646,13 +618,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" - path_provider: - dependency: transitive - description: - name: path_provider - url: "https://pub.dartlang.org" - source: hosted - version: "1.6.27" path_provider_linux: dependency: transitive description: @@ -660,13 +625,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+2" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.4+8" path_provider_platform_interface: dependency: transitive description: @@ -800,13 +758,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" - rxdart: - dependency: transitive - description: - name: rxdart - url: "https://pub.dartlang.org" - source: hosted - version: "0.25.0" screen: dependency: "direct main" description: @@ -869,21 +820,21 @@ packages: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.9+1" + version: "0.2.9+2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "0.2.4" sky_engine: dependency: transitive description: flutter @@ -1063,7 +1014,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.5+1" + version: "0.1.5+3" url_launcher_windows: dependency: transitive description: @@ -1071,13 +1022,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+3" - uuid: - dependency: transitive - description: - name: uuid - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.2" validate: dependency: transitive description: @@ -1140,7 +1084,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.4" + version: "1.7.4+1" wkt_parser: dependency: transitive description: From 87bb1327df021787c00cd596e807ef9bed78e6df Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 31 Jan 2021 14:57:35 +0900 Subject: [PATCH 42/44] viewer: fixed view and minimap refresh after rotation --- lib/widgets/viewer/overlay/minimap.dart | 25 ++++++------ .../viewer/visual/entry_page_view.dart | 38 +++++++++++-------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index 794de067a..3aea62b92 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -35,29 +35,32 @@ class Minimap extends StatelessWidget { valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { final pageEntry = mainEntry.getPageEntry(multiPageInfo?.getByIndex(page)); - return _buildForEntrySize(pageEntry.displaySize); + return _buildForEntrySize(pageEntry); }, ); }) - : _buildForEntrySize(mainEntry.displaySize), + : _buildForEntrySize(mainEntry), ); } - Widget _buildForEntrySize(Size entrySize) { + Widget _buildForEntrySize(AvesEntry entry) { return ValueListenableBuilder( valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; if (viewportSize == null) return SizedBox.shrink(); - return CustomPaint( - painter: MinimapPainter( - viewportSize: viewportSize, - entrySize: entrySize, - viewCenterOffset: viewState.position, - viewScale: viewState.scale, - minimapBorderColor: Colors.white30, + 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, ), - size: size, ); }); } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index ea17a2478..ba18f7473 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -111,7 +111,7 @@ class _EntryPageViewState extends State { } void _unregisterWidget() { - _magnifierController.dispose(); + _magnifierController?.dispose(); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -119,19 +119,25 @@ class _EntryPageViewState extends State { @override Widget build(BuildContext context) { - Widget child; - if (entry.isVideo) { - if (!entry.displaySize.isEmpty) { - child = _buildVideoView(); - } - } else if (entry.isSvg) { - child = _buildSvgView(); - } else if (entry.canDecode) { - child = _buildRasterView(); - } - child ??= ErrorView( - entry: entry, - onTap: () => onTap?.call(null), + final child = AnimatedBuilder( + animation: entry.imageChangeNotifier, + builder: (context, child) { + Widget child; + if (entry.isVideo) { + if (!entry.displaySize.isEmpty) { + child = _buildVideoView(); + } + } else if (entry.isSvg) { + child = _buildSvgView(); + } else if (entry.canDecode) { + child = _buildRasterView(); + } + child ??= ErrorView( + entry: entry, + onTap: () => onTap?.call(null), + ); + return child; + }, ); return widget.heroTag != null @@ -201,8 +207,8 @@ class _EntryPageViewState extends State { @required Widget child, }) { return Magnifier( - // key includes size and orientation to refresh when the image is rotated - key: ValueKey('${entry.pageId}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), + // 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, minScale: minScale, From 4cfaf9ae8879c5e2264d2835dc0164d58745d674 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 31 Jan 2021 15:13:26 +0900 Subject: [PATCH 43/44] updated changelog --- CHANGELOG.md | 14 +++++++++++++- README.md | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec203e5be..1b74d7866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,20 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- Viewer: support for multi-track HEIF +- Viewer: export image (including multipage TIFF/HEIF and images embedded in XMP) +- Info: show owner app (Android Q and up) +- listen to Media Store changes + ### Changed -- Upgraded Flutter to stable v1.22.6 +- upgraded Flutter to stable v1.22.6 +- check connectivity before using features that need it + +### Fixed +- checkerboard background performance +- deleting files that no longer exist but are still registered in the Media Store +- insets handling on Android 11 ## [v1.3.2] - 2021-01-17 ### Added diff --git a/README.md b/README.md index fbb336a5a..34fbae115 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt ## Features -- support raster images: JPEG, GIF, PNG, HEIC (from Android Pie), WEBP, TIFF (including multi-page), BMP, WBMP, ICO +- support raster images: JPEG, GIF, PNG, HEIC/HEIF (including multi-track, from Android Pie), WEBP, TIFF (including multi-page), BMP, WBMP, ICO - support animated images: GIF, WEBP - support raw images: ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW - support vector images: SVG From aab4800d9b7a646930eef8a16607a5095fafeed8 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 31 Jan 2021 15:20:45 +0900 Subject: [PATCH 44/44] version bump --- CHANGELOG.md | 2 ++ pubspec.yaml | 2 +- whatsnew/whatsnew-en-US | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b74d7866..44e021228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] + +## [v1.3.3] - 2021-01-31 ### Added - Viewer: support for multi-track HEIF - Viewer: export image (including multipage TIFF/HEIF and images embedded in XMP) diff --git a/pubspec.yaml b/pubspec.yaml index 7dbfd751f..19b833434 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Aves is a gallery and metadata explorer app, built for Android. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.3.2+38 +version: 1.3.3+39 # brendan-duncan/image (as of v2.1.19): # - does not support TIFF with JPEG compression (issue #184) diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 5dac30365..61628d128 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,6 +1,6 @@ Thanks for using Aves! -v1.3.2: -- multi-page TIFF support -- cropped panorama support -- album grouping options +v1.3.3: +- multi-track HEIF support +- image export (including embedded and multi-page images) +- listen to Media Store changes Full changelog available on Github \ No newline at end of file