diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a24b65126..2fc1c8500 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: '2.8.1' + flutter-version: '2.10.1' - name: Clone the repository. uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db2cefafe..5ea3f4a01 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: '2.8.1' + flutter-version: '2.10.1' # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): # https://issuetracker.google.com/issues/144111441 @@ -52,12 +52,12 @@ jobs: rm release.keystore.asc mkdir outputs (cd scripts/; ./apply_flavor_play.sh) - flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.8.1.sksl.json + flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.10.1.sksl.json cp build/app/outputs/bundle/playRelease/*.aab outputs - flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.8.1.sksl.json + flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.10.1.sksl.json cp build/app/outputs/apk/play/release/*.apk outputs (cd scripts/; ./apply_flavor_izzy.sh) - flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_2.8.1.sksl.json + flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_2.10.1.sksl.json cp build/app/outputs/apk/izzy/release/*.apk outputs rm $AVES_STORE_FILE env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4465e83..b799ded82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.6.0] - 2022-02-22 + +### Added + +- optional recycle bin to keep deleted items for 30 days +- Viewer: actions to copy/move to album +- Indonesian translation (thanks MeFinity) + +### Changed + +- Viewer: action menu reorganization +- Viewer: `Export` action renamed to `Convert` +- Viewer: actual size zoom level respects device pixel ratio +- Viewer: allow zooming out small items to actual size +- Collection: improved performance for sort/group by name +- load previous top items on startup +- locale independent colors for known filters +- upgraded Flutter to stable v2.10.1 + +### Removed + +- Map: connectivity check + +### Fixed + +- navigating from Album page when picking an item for another app + ## [v1.5.11] - 2022-01-30 ### Added diff --git a/README.md b/README.md index c7dec61cd..4f17a28bb 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ At this stage this project does *not* accept PRs, except for translations. ### Translations -If you want to translate this app in your language and share the result, [there is a guide](https://github.com/deckerst/aves/wiki/Contributing-to-Translations). English, Korean and French are already handled by me. Russian, German and Spanish are handled by generous volunteers. +If you want to translate this app in your language and share the result, [there is a guide](https://github.com/deckerst/aves/wiki/Contributing-to-Translations). English, Korean and French are already handled by me. Russian, German, Spanish, Portuguese & Indonesian are handled by generous volunteers. ### Donations diff --git a/android/app/build.gradle b/android/app/build.gradle index 8acc2d333..fed4ca45f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -141,7 +141,7 @@ repositories { } dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.multidex:multidex:2.0.1' @@ -152,10 +152,10 @@ dependencies { implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android implementation 'com.github.deckerst:pixymeta-android:706bd73d6e' - implementation 'com.github.bumptech.glide:glide:4.12.0' + implementation 'com.github.bumptech.glide:glide:4.13.0' kapt 'androidx.annotation:annotation:1.3.0' - kapt 'com.github.bumptech.glide:compiler:4.12.0' + kapt 'com.github.bumptech.glide:compiler:4.13.0' compileOnly rootProject.findProject(':streams_channel') } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt index 409f361bd..01752c888 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt @@ -159,10 +159,10 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { COMMAND_START -> { runBlocking { FlutterUtils.runOnUiThread { - val contentIds = data.get(KEY_CONTENT_IDS)?.takeIf { it is IntArray }?.let { (it as IntArray).toList() } + val entryIds = data.get(KEY_ENTRY_IDS)?.takeIf { it is IntArray }?.let { (it as IntArray).toList() } backgroundChannel?.invokeMethod( "start", hashMapOf( - "contentIds" to contentIds, + "entryIds" to entryIds, "force" to data.getBoolean(KEY_FORCE), ) ) @@ -197,7 +197,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { const val KEY_COMMAND = "command" const val COMMAND_START = "start" const val COMMAND_STOP = "stop" - const val KEY_CONTENT_IDS = "content_ids" + const val KEY_ENTRY_IDS = "entry_ids" const val KEY_FORCE = "force" } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt index ca9ba2fa7..8d4472dbe 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt @@ -17,15 +17,15 @@ import deckers.thibault.aves.utils.LogUtils import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import java.util.* import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvider() { + private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? { return selectionArgs?.firstOrNull()?.let { query -> // Samsung Finder does not support: @@ -77,29 +77,34 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL) backgroundChannel.setMethodCallHandler(this) - return suspendCoroutine { cont -> - GlobalScope.launch { - FlutterUtils.runOnUiThread { - backgroundChannel.invokeMethod("getSuggestions", hashMapOf( - "query" to query, - "locale" to Locale.getDefault().toString(), - "use24hour" to DateFormat.is24HourFormat(context), - ), object : MethodChannel.Result { - override fun success(result: Any?) { - @Suppress("unchecked_cast") - cont.resume(result as List) - } + try { + return suspendCoroutine { cont -> + defaultScope.launch { + FlutterUtils.runOnUiThread { + backgroundChannel.invokeMethod("getSuggestions", hashMapOf( + "query" to query, + "locale" to Locale.getDefault().toString(), + "use24hour" to DateFormat.is24HourFormat(context), + ), object : MethodChannel.Result { + override fun success(result: Any?) { + @Suppress("unchecked_cast") + cont.resume(result as List) + } - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails")) - } + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails")) + } - override fun notImplemented() { - cont.resumeWithException(NotImplementedError("getSuggestions")) - } - }) + override fun notImplemented() { + cont.resumeWithException(NotImplementedError("getSuggestions")) + } + }) + } } } + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to get suggestions", e) + return ArrayList() } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt index b0d144906..7f083ae55 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt @@ -16,14 +16,14 @@ import deckers.thibault.aves.utils.ContextUtils.isMyServiceRunning import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* class AnalysisHandler(private val activity: Activity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler, AnalysisServiceListener { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "registerCallback" -> GlobalScope.launch(Dispatchers.IO) { Coresult.safe(call, result, ::registerCallback) } + "registerCallback" -> ioScope.launch { Coresult.safe(call, result, ::registerCallback) } "startService" -> Coresult.safe(call, result, ::startAnalysis) else -> result.notImplemented() } @@ -52,12 +52,12 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp } // can be null or empty - val contentIds = call.argument>("contentIds") + val entryIds = call.argument>("entryIds") if (!activity.isMyServiceRunning(AnalysisService::class.java)) { val intent = Intent(activity, AnalysisService::class.java) intent.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START) - intent.putExtra(AnalysisService.KEY_CONTENT_IDS, contentIds?.toIntArray()) + intent.putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray()) intent.putExtra(AnalysisService.KEY_FORCE, force) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { activity.startForegroundService(intent) 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 f56a7f838..ee0527d6c 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 @@ -33,26 +33,26 @@ import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.io.File import java.util.* import kotlin.collections.ArrayList import kotlin.math.roundToInt class AppAdapterHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) } - "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getAppIcon) } - "copyToClipboard" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::copyToClipboard) } + "getPackages" -> ioScope.launch { safe(call, result, ::getPackages) } + "getAppIcon" -> ioScope.launch { safeSuspend(call, result, ::getAppIcon) } + "copyToClipboard" -> ioScope.launch { safe(call, result, ::copyToClipboard) } "edit" -> safe(call, result, ::edit) "open" -> safe(call, result, ::open) "openMap" -> safe(call, result, ::openMap) "setAs" -> safe(call, result, ::setAs) "share" -> safe(call, result, ::share) - "pinShortcut" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pinShortcut) } + "pinShortcut" -> ioScope.launch { safe(call, result, ::pinShortcut) } else -> result.notImplemented() } } 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 4aebabf55..82c84520e 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 @@ -32,33 +32,33 @@ import deckers.thibault.aves.utils.UriUtils.tryParseId import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import io.flutter.util.PathUtils +import kotlinx.coroutines.* import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.IOException -import java.util.* class DebugHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "crash" -> Handler(Looper.getMainLooper()).postDelayed({ throw TestException() }, 50) "exception" -> throw TestException() "safeException" -> safe(call, result) { _, _ -> throw TestException() } - "exceptionInCoroutine" -> GlobalScope.launch(Dispatchers.IO) { throw TestException() } - "safeExceptionInCoroutine" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result) { _, _ -> throw TestException() } } + "exceptionInCoroutine" -> ioScope.launch { throw TestException() } + "safeExceptionInCoroutine" -> ioScope.launch { safe(call, result) { _, _ -> throw TestException() } } - "getContextDirs" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContextDirs) } + "getContextDirs" -> ioScope.launch { safe(call, result, ::getContextDirs) } "getCodecs" -> safe(call, result, ::getCodecs) "getEnv" -> safe(call, result, ::getEnv) - "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) } - "getPixyMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPixyMetadata) } - "getTiffStructure" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getTiffStructure) } + "getBitmapFactoryInfo" -> ioScope.launch { safe(call, result, ::getBitmapFactoryInfo) } + "getContentResolverMetadata" -> ioScope.launch { safe(call, result, ::getContentResolverMetadata) } + "getExifInterfaceMetadata" -> ioScope.launch { safe(call, result, ::getExifInterfaceMetadata) } + "getMediaMetadataRetrieverMetadata" -> ioScope.launch { safe(call, result, ::getMediaMetadataRetrieverMetadata) } + "getMetadataExtractorSummary" -> ioScope.launch { safe(call, result, ::getMetadataExtractorSummary) } + "getPixyMetadata" -> ioScope.launch { safe(call, result, ::getPixyMetadata) } + "getTiffStructure" -> ioScope.launch { safe(call, result, ::getTiffStructure) } else -> result.notImplemented() } } @@ -69,6 +69,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { "filesDir" to context.filesDir, "obbDir" to context.obbDir, "externalCacheDir" to context.externalCacheDir, + "externalFilesDir" to context.getExternalFilesDir(null), ).apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { putAll( @@ -81,7 +82,18 @@ class DebugHandler(private val context: Context) : MethodCallHandler { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { put("dataDir", context.dataDir) } - }.mapValues { it.value?.path } + }.mapValues { it.value?.path }.toMutableMap() + dirs["externalCacheDirs"] = context.externalCacheDirs.joinToString { it.path } + dirs["externalFilesDirs"] = context.getExternalFilesDirs(null).joinToString { it.path } + + // used by flutter plugin `path_provider` + dirs.putAll( + hashMapOf( + "flutter / cacheDir" to PathUtils.getCacheDirectory(context), + "flutter / dataDir" to PathUtils.getDataDirectory(context), + "flutter / filesDir" to PathUtils.getFilesDir(context), + ) + ) result.success(dirs) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index 61fa7d5c0..d34ef9eec 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -34,20 +34,20 @@ import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.io.File import java.io.InputStream import java.util.* class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getExifThumbnails) } - "extractMotionPhotoVideo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractMotionPhotoVideo) } - "extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) } - "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) } + "getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) } + "extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) } + "extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) } + "extractXmpDataProp" -> ioScope.launch { safe(call, result, ::extractXmpDataProp) } else -> result.notImplemented() } } @@ -210,7 +210,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { "mimeType" to mimeType, ) if (isImage(mimeType) || isVideo(mimeType)) { - GlobalScope.launch(Dispatchers.IO) { + ioScope.launch { ContentImageProvider().fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback { override fun onSuccess(fields: FieldMap) { resultFields.putAll(fields) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt index ddb100edd..0821058f3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt @@ -6,9 +6,7 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.io.IOException import java.util.* @@ -16,11 +14,12 @@ import java.util.* // - `geocoder` is unmaintained // - `geocoding` method does not return `addressLine` (v2.0.0) class GeocodingHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var geocoder: Geocoder? = null override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getAddress" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAddress) } + "getAddress" -> ioScope.launch { safe(call, result, ::getAddress) } else -> result.notImplemented() } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt index 48560f9a5..c4b701327 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt @@ -7,14 +7,14 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* class GlobalSearchHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "registerCallback" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::registerCallback) } + "registerCallback" -> ioScope.launch { safe(call, result, ::registerCallback) } else -> result.notImplemented() } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt index 9dddda6b1..44c0cc405 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFileHandler.kt @@ -21,24 +21,23 @@ import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlin.math.roundToInt class MediaFileHandler(private val activity: Activity) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val density = activity.resources.displayMetrics.density private val regionFetcher = RegionFetcher(activity) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEntry) } - "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getThumbnail) } - "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getRegion) } + "getEntry" -> ioScope.launch { safe(call, result, ::getEntry) } + "getThumbnail" -> ioScope.launch { safeSuspend(call, result, ::getThumbnail) } + "getRegion" -> ioScope.launch { safeSuspend(call, result, ::getRegion) } "cancelFileOp" -> safe(call, result, ::cancelFileOp) - "captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::captureFrame) } - "clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) } + "captureFrame" -> ioScope.launch { safeSuspend(call, result, ::captureFrame) } + "clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) } else -> result.notImplemented() } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaStoreHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaStoreHandler.kt index b20b6b802..a8761be8f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaStoreHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaStoreHandler.kt @@ -8,22 +8,25 @@ import deckers.thibault.aves.model.provider.MediaStoreImageProvider import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch class MediaStoreHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "checkObsoleteContentIds" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::checkObsoleteContentIds) } - "checkObsoletePaths" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::checkObsoletePaths) } - "scanFile" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::scanFile) } + "checkObsoleteContentIds" -> ioScope.launch { safe(call, result, ::checkObsoleteContentIds) } + "checkObsoletePaths" -> ioScope.launch { safe(call, result, ::checkObsoletePaths) } + "scanFile" -> ioScope.launch { safe(call, result, ::scanFile) } else -> result.notImplemented() } } private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) { - val knownContentIds = call.argument>("knownContentIds") + val knownContentIds = call.argument>("knownContentIds") if (knownContentIds == null) { result.error("checkObsoleteContentIds-args", "failed because of missing arguments", null) return @@ -32,7 +35,7 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler { } private fun checkObsoletePaths(call: MethodCall, result: MethodChannel.Result) { - val knownPathById = call.argument>("knownPathById") + val knownPathById = call.argument>("knownPathById") if (knownPathById == null) { result.error("checkObsoletePaths-args", "failed because of missing arguments", null) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt index adc3449f5..1e572f0c5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt @@ -10,18 +10,18 @@ import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) } - "flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) } - "editDate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::editDate) } - "editMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::editMetadata) } - "removeTypes" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::removeTypes) } + "rotate" -> ioScope.launch { safe(call, result, ::rotate) } + "flip" -> ioScope.launch { safe(call, result, ::flip) } + "editDate" -> ioScope.launch { safe(call, result, ::editDate) } + "editMetadata" -> ioScope.launch { safe(call, result, ::editMetadata) } + "removeTypes" -> ioScope.launch { safe(call, result, ::removeTypes) } else -> result.notImplemented() } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index b461e2b9f..e70895276 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -74,29 +74,28 @@ import deckers.thibault.aves.utils.UriUtils.tryParseId import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.text.ParseException -import java.util.* import kotlin.math.roundToInt import kotlin.math.roundToLong class MetadataFetchHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "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) } - "getIptc" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getIptc) } - "getXmp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getXmp) } - "hasContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::hasContentResolverProp) } - "getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) } - "getDate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getDate) } + "getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) } + "getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) } + "getOverlayMetadata" -> ioScope.launch { safe(call, result, ::getOverlayMetadata) } + "getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) } + "getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) } + "getIptc" -> ioScope.launch { safe(call, result, ::getIptc) } + "getXmp" -> ioScope.launch { safe(call, result, ::getXmp) } + "hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentResolverProp) } + "getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentResolverProp) } + "getDate" -> ioScope.launch { safe(call, result, ::getDate) } else -> result.notImplemented() } } @@ -412,19 +411,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { // File type for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) { - // * `metadata-extractor` sometimes detects the wrong MIME type (e.g. `pef` file as `tiff`, `mpeg` as `dvd`) + // * `metadata-extractor` sometimes detects the wrong MIME type (e.g. `pef` file as `tiff`, `mpeg` as `dvd`, `avif` as `mov`) // * the content resolver / media store sometimes reports the wrong MIME type (e.g. `png` file as `jpeg`, `tiff` as `srw`) // * `context.getContentResolver().getType()` sometimes returns an incorrect value // * `MediaMetadataRetriever.setDataSource()` sometimes fails with `status = 0x80000000` // * file extension is unreliable - // In the end, `metadata-extractor` is the most reliable, except for `tiff`/`dvd` (false positives, false negatives), + // In the end, `metadata-extractor` is the most reliable, except for `tiff`/`dvd`/`mov` (false positives, false negatives), // in which case we trust the file extension // cf https://github.com/drewnoakes/metadata-extractor/issues/296 if (path?.matches(TIFF_EXTENSION_PATTERN) == true) { metadataMap[KEY_MIME_TYPE] = MimeTypes.TIFF } else { dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { - if (it != MimeTypes.TIFF && it != MimeTypes.DVD) { + if (it != MimeTypes.TIFF && it != MimeTypes.DVD && it != MimeTypes.MOV) { metadataMap[KEY_MIME_TYPE] = it } } 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 c8bb7a1db..9fc6adbc6 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 @@ -13,22 +13,24 @@ import deckers.thibault.aves.utils.StorageUtils.getVolumePaths import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import java.io.File -import java.util.* class StorageHandler(private val context: Context) : MethodCallHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getStorageVolumes" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getStorageVolumes) } - "getFreeSpace" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getFreeSpace) } - "getGrantedDirectories" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getGrantedDirectories) } - "getInaccessibleDirectories" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getInaccessibleDirectories) } - "getRestrictedDirectories" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getRestrictedDirectories) } + "getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) } + "getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) } + "getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) } + "getInaccessibleDirectories" -> ioScope.launch { safe(call, result, ::getInaccessibleDirectories) } + "getRestrictedDirectories" -> ioScope.launch { safe(call, result, ::getRestrictedDirectories) } "revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess) - "deleteEmptyDirectories" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::deleteEmptyDirectories) } + "deleteEmptyDirectories" -> ioScope.launch { safe(call, result, ::deleteEmptyDirectories) } "canRequestMediaFileBulkAccess" -> safe(call, result, ::canRequestMediaFileBulkAccess) "canInsertMedia" -> safe(call, result, ::canInsertMedia) else -> result.notImplemented() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt index 68c1c5293..5e99cee7e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt @@ -131,7 +131,7 @@ class ThumbnailFetcher internal constructor( svgFetch -> SvgImage(context, uri) tiffFetch -> TiffImage(context, uri, pageId) multiTrackFetch -> MultiTrackImage(context, uri, pageId) - else -> StorageUtils.getGlideSafeUri(uri, mimeType) + else -> StorageUtils.getGlideSafeUri(context, uri, mimeType) } 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 5ff445aaa..7e58f4ed0 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 @@ -23,12 +23,11 @@ import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.io.InputStream class ImageByteStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private lateinit var eventSink: EventSink private lateinit var handler: Handler @@ -36,7 +35,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments this.eventSink = eventSink handler = Handler(Looper.getMainLooper()) - GlobalScope.launch(Dispatchers.IO) { streamImage() } + ioScope.launch { streamImage() } } override fun onCancel(o: Any) {} @@ -119,7 +118,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments } else if (mimeType == MimeTypes.TIFF) { TiffImage(context, uri, pageId) } else { - StorageUtils.getGlideSafeUri(uri, mimeType) + StorageUtils.getGlideSafeUri(context, uri, mimeType) } val target = Glide.with(context) 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 a574fb74b..6d8b1a176 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 @@ -11,16 +11,19 @@ import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.NameConflictStrategy import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider +import deckers.thibault.aves.model.provider.MediaStoreImageProvider import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import java.util.* +import java.io.File class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private lateinit var eventSink: EventSink private lateinit var handler: Handler @@ -45,10 +48,10 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments handler = Handler(Looper.getMainLooper()) when (op) { - "delete" -> GlobalScope.launch(Dispatchers.IO) { delete() } - "export" -> GlobalScope.launch(Dispatchers.IO) { export() } - "move" -> GlobalScope.launch(Dispatchers.IO) { move() } - "rename" -> GlobalScope.launch(Dispatchers.IO) { rename() } + "delete" -> ioScope.launch { delete() } + "export" -> ioScope.launch { export() } + "move" -> ioScope.launch { move() } + "rename" -> ioScope.launch { rename() } else -> endOfStream() } } @@ -103,12 +106,16 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments val entries = entryMapList.map(::AvesEntry) for (entry in entries) { - val uri = entry.uri - val path = entry.path val mimeType = entry.mimeType + val trashed = entry.trashed + + val uri = if (trashed) Uri.fromFile(File(entry.trashPath!!)) else entry.uri + val path = if (trashed) entry.trashPath else entry.path val result: FieldMap = hashMapOf( - "uri" to uri.toString(), + // `uri` should reference original content URI, + // so it is different with `sourceUri` when deleting trashed entries + "uri" to entry.uri.toString(), ) if (isCancelledOp()) { result["skipped"] = true @@ -160,30 +167,34 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments } private suspend fun move() { - if (arguments !is Map<*, *> || entryMapList.isEmpty()) { + if (arguments !is Map<*, *>) { endOfStream() return } val copy = arguments["copy"] as Boolean? - var destinationDir = arguments["destinationPath"] as String? val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) - if (copy == null || destinationDir == null || nameConflictStrategy == null) { + val rawEntryMap = arguments["entriesByDestination"] as Map<*, *>? + if (copy == null || nameConflictStrategy == null || rawEntryMap == null || rawEntryMap.isEmpty()) { 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 + val entriesByTargetDir = HashMap>() + rawEntryMap.forEach { + var destinationDir = it.key as String + if (destinationDir != StorageUtils.TRASH_PATH_PLACEHOLDER) { + destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) + } + @Suppress("unchecked_cast") + val rawEntries = it.value as List + entriesByTargetDir[destinationDir] = rawEntries.map(::AvesEntry) } - destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) - val entries = entryMapList.map(::AvesEntry) - provider.moveMultiple(activity, copy, destinationDir, nameConflictStrategy, entries, ::isCancelledOp, object : ImageOpCallback { + // always use Media Store (as we move from or to it) + val provider = MediaStoreImageProvider() + + provider.moveMultiple(activity, copy, nameConflictStrategy, entriesByTargetDir, ::isCancelledOp, 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/channel/streams/MediaStoreStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt index f90e5971b..54b79630b 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 @@ -9,20 +9,24 @@ import deckers.thibault.aves.model.provider.MediaStoreImageProvider import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : EventChannel.StreamHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private lateinit var eventSink: EventSink private lateinit var handler: Handler - private var knownEntries: Map? = null + private var knownEntries: Map? = null + private var directory: String? = null init { if (arguments is Map<*, *>) { @Suppress("unchecked_cast") - knownEntries = arguments["knownEntries"] as Map? + knownEntries = arguments["knownEntries"] as Map? + directory = arguments["directory"] as String? } } @@ -30,7 +34,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E this.eventSink = eventSink handler = Handler(Looper.getMainLooper()) - GlobalScope.launch(Dispatchers.IO) { fetchAll() } + ioScope.launch { fetchAll() } } override fun onCancel(arguments: Any?) {} @@ -56,7 +60,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E } private fun fetchAll() { - MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap()) { success(it) } + MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) } endOfStream() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt index 6d134fae4..0bc307f92 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt @@ -15,14 +15,16 @@ import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.PermissionManager import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import java.io.FileOutputStream // starting activity to give access with the native dialog // breaks the regular `MethodChannel` so we use a stream channel instead class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?) : EventChannel.StreamHandler { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private lateinit var eventSink: EventSink private lateinit var handler: Handler @@ -41,10 +43,10 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? handler = Handler(Looper.getMainLooper()) when (op) { - "requestDirectoryAccess" -> GlobalScope.launch(Dispatchers.IO) { requestDirectoryAccess() } - "requestMediaFileAccess" -> GlobalScope.launch(Dispatchers.IO) { requestMediaFileAccess() } - "createFile" -> GlobalScope.launch(Dispatchers.IO) { createFile() } - "openFile" -> GlobalScope.launch(Dispatchers.IO) { openFile() } + "requestDirectoryAccess" -> ioScope.launch { requestDirectoryAccess() } + "requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() } + "createFile" -> ioScope.launch { createFile() } + "openFile" -> ioScope.launch { openFile() } else -> endOfStream() } } @@ -113,7 +115,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? putExtra(Intent.EXTRA_TITLE, name) } MainActivity.pendingStorageAccessResultHandlers[MainActivity.CREATE_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri -> - GlobalScope.launch(Dispatchers.IO) { + ioScope.launch { try { activity.contentResolver.openOutputStream(uri)?.use { output -> output as FileOutputStream @@ -145,7 +147,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? val mimeType = args["mimeType"] as String? // optional fun onGranted(uri: Uri) { - GlobalScope.launch(Dispatchers.IO) { + ioScope.launch { activity.contentResolver.openInputStream(uri)?.use { input -> val buffer = ByteArray(BUFFER_SIZE) var len: Int 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 6c548ef44..f13a85f8e 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 @@ -18,8 +18,9 @@ import com.bumptech.glide.module.LibraryGlideModule import com.bumptech.glide.signature.ObjectKey import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import java.io.ByteArrayInputStream import java.io.InputStream @@ -48,8 +49,10 @@ internal class VideoThumbnailLoader : ModelLoader { } internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher { + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + override fun loadData(priority: Priority, callback: DataCallback) { - GlobalScope.launch(Dispatchers.IO) { + ioScope.launch { val retriever = openMetadataRetriever(model.context, model.uri) if (retriever == null) { callback.onLoadFailed(Exception("failed to initialize MediaMetadataRetriever for uri=${model.uri}")) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt index 23f5e9020..e2a1c45e4 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt @@ -62,7 +62,7 @@ class GSpherical(xmlBytes: ByteArray) { } } - fun describe(): Map = hashMapOf( + fun describe(): Map = hashMapOf( "Spherical" to spherical.toString(), "Stitched" to stitched.toString(), "Stitching Software" to stitchingSoftware, @@ -79,7 +79,7 @@ class GSpherical(xmlBytes: ByteArray) { "Cropped Area Image Height Pixels" to croppedAreaImageHeightPixels?.toString(), "Cropped Area Left Pixels" to croppedAreaLeftPixels?.toString(), "Cropped Area Top Pixels" to croppedAreaTopPixels?.toString(), - ).filterValues { it != null } + ).filterValues { it != null }.mapValues { it.value as String } companion object SphericalVideo { private val LOG_TAG = LogUtils.createTag() 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 ab6f58ce9..54aded97e 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 @@ -11,4 +11,6 @@ class AvesEntry(map: FieldMap) { val height = map["height"] as Int val rotationDegrees = map["rotationDegrees"] as Int val isFlipped = map["isFlipped"] as Boolean + val trashed = map["trashed"] as Boolean + val trashPath = map["trashPath"] as String? } \ No newline at end of file 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 eb0eb3138..7a6cfae94 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 @@ -1,8 +1,11 @@ package deckers.thibault.aves.model.provider +import android.app.Activity import android.content.Context import android.net.Uri +import android.util.Log import deckers.thibault.aves.model.SourceEntry +import deckers.thibault.aves.utils.LogUtils import java.io.File internal class FileImageProvider : ImageProvider() { @@ -33,4 +36,18 @@ internal class FileImageProvider : ImageProvider() { callback.onFailure(Exception("entry has no size")) } } + + override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { + val file = File(File(uri.path!!).path) + if (!file.exists()) return + + Log.d(LOG_TAG, "delete file at uri=$uri") + if (file.delete()) return + + throw Exception("failed to delete entry with uri=$uri path=$path") + } + + companion object { + private val LOG_TAG = LogUtils.createTag() + } } \ 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 0466552bd..43e648b34 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 @@ -39,7 +39,6 @@ import java.io.File import java.io.IOException import java.io.OutputStream import java.util.* -import kotlin.collections.HashMap abstract class ImageProvider { open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { @@ -53,9 +52,8 @@ abstract class ImageProvider { open suspend fun moveMultiple( activity: Activity, copy: Boolean, - targetDir: String, nameConflictStrategy: NameConflictStrategy, - entries: List, + entriesByTargetDir: Map>, isCancelledOp: CancelCheck, callback: ImageOpCallback, ) { @@ -133,7 +131,6 @@ abstract class ImageProvider { } } - @Suppress("BlockingMethodInNonBlockingContext") private suspend fun exportSingle( activity: Activity, sourceEntry: AvesEntry, @@ -174,6 +171,7 @@ abstract class ImageProvider { targetMimeType = sourceMimeType write = { output -> val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri) + @Suppress("BlockingMethodInNonBlockingContext") sourceDocFile.copyTo(output) } } else { @@ -184,7 +182,7 @@ abstract class ImageProvider { } else if (sourceMimeType == MimeTypes.SVG) { SvgImage(activity, sourceUri) } else { - StorageUtils.getGlideSafeUri(sourceUri, sourceMimeType) + StorageUtils.getGlideSafeUri(activity, sourceUri, sourceMimeType) } // request a fresh image with the highest quality format @@ -198,6 +196,7 @@ abstract class ImageProvider { .apply(glideOptions) .load(model) .submit(width, height) + @Suppress("BlockingMethodInNonBlockingContext") var bitmap = target.get() if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) @@ -244,7 +243,6 @@ abstract class ImageProvider { // clearing Glide target should happen after effectively writing the bitmap Glide.with(activity).clear(target) } - } @Suppress("BlockingMethodInNonBlockingContext") 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 4ada34d6f..c17870a08 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,6 +3,7 @@ package deckers.thibault.aves.model.provider import android.annotation.SuppressLint import android.app.Activity import android.app.RecoverableSecurityException +import android.content.ContentResolver import android.content.ContentUris import android.content.ContentValues import android.content.Context @@ -26,24 +27,55 @@ import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils.PathSegments +import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator +import deckers.thibault.aves.utils.StorageUtils.removeTrailingSeparator import deckers.thibault.aves.utils.UriUtils.tryParseId import java.io.File import java.io.OutputStream import java.util.* import java.util.concurrent.CompletableFuture -import kotlin.collections.ArrayList import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine class MediaStoreImageProvider : ImageProvider() { - fun fetchAll(context: Context, knownEntries: Map, handleNewEntry: NewEntryHandler) { + fun fetchAll( + context: Context, + knownEntries: Map, + directory: String?, + handleNewEntry: NewEntryHandler, + ) { val isModified = fun(contentId: Int, dateModifiedSecs: Int): Boolean { val knownDate = knownEntries[contentId] return knownDate == null || knownDate < dateModifiedSecs } - fetchFrom(context, isModified, handleNewEntry, IMAGE_CONTENT_URI, IMAGE_PROJECTION) - fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION) + val handleNew: NewEntryHandler + var selection: String? = null + var selectionArgs: Array? = null + if (directory != null) { + val relativePathDirectory = ensureTrailingSeparator(directory) + val relativePath = PathSegments(context, relativePathDirectory).relativeDir + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && relativePath != null) { + selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaColumns.PATH} LIKE ?" + selectionArgs = arrayOf(relativePath, "relativePathDirectory%") + } else { + selection = "${MediaColumns.PATH} LIKE ?" + selectionArgs = arrayOf("$relativePathDirectory%") + } + + val parentCheckDirectory = removeTrailingSeparator(directory) + handleNew = { entry -> + // skip entries in subfolders + val path = entry["path"] as String? + if (path != null && File(path).parent == parentCheckDirectory) { + handleNewEntry(entry) + } + } + } else { + handleNew = handleNewEntry + } + fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs) + fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs) } // the provided URI can point to the wrong media collection, @@ -83,7 +115,7 @@ class MediaStoreImageProvider : ImageProvider() { } } - fun checkObsoleteContentIds(context: Context, knownContentIds: List): List { + fun checkObsoleteContentIds(context: Context, knownContentIds: List): List { val foundContentIds = HashSet() fun check(context: Context, contentUri: Uri) { val projection = arrayOf(MediaStore.MediaColumns._ID) @@ -102,10 +134,10 @@ class MediaStoreImageProvider : ImageProvider() { } check(context, IMAGE_CONTENT_URI) check(context, VIDEO_CONTENT_URI) - return knownContentIds.subtract(foundContentIds).toList() + return knownContentIds.subtract(foundContentIds).filterNotNull().toList() } - fun checkObsoletePaths(context: Context, knownPathById: Map): List { + fun checkObsoletePaths(context: Context, knownPathById: Map): List { val obsoleteIds = ArrayList() fun check(context: Context, contentUri: Uri) { val projection = arrayOf(MediaStore.MediaColumns._ID, MediaColumns.PATH) @@ -138,12 +170,14 @@ class MediaStoreImageProvider : ImageProvider() { handleNewEntry: NewEntryHandler, contentUri: Uri, projection: Array, + selection: String? = null, + selectionArgs: Array? = null, fileMimeType: String? = null, ): Boolean { var found = false val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC" try { - val cursor = context.contentResolver.query(contentUri, projection, null, null, orderBy) + val cursor = context.contentResolver.query(contentUri, projection, selection, selectionArgs, orderBy) if (cursor != null) { val contentUriContainsId = when (contentUri) { IMAGE_CONTENT_URI, VIDEO_CONTENT_URI -> false @@ -291,6 +325,10 @@ class MediaStoreImageProvider : ImageProvider() { } throw Exception("failed to delete document with df=$df") } + } else if (uri.scheme?.lowercase(Locale.ROOT) == ContentResolver.SCHEME_FILE) { + val uriFilePath = File(uri.path!!).path + // URI and path both point to the same non existent path + if (uriFilePath == path) return } try { @@ -329,84 +367,119 @@ class MediaStoreImageProvider : ImageProvider() { override suspend fun moveMultiple( activity: Activity, copy: Boolean, - targetDir: String, nameConflictStrategy: NameConflictStrategy, - entries: List, + entriesByTargetDir: Map>, isCancelledOp: CancelCheck, callback: ImageOpCallback, ) { - val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) - if (!File(targetDir).exists()) { - callback.onFailure(Exception("failed to create directory at path=$targetDir")) - return - } + entriesByTargetDir.forEach { kv -> + val targetDir = kv.key + val entries = kv.value - for (entry in entries) { - val sourceUri = entry.uri - val sourcePath = entry.path - val mimeType = entry.mimeType + val toBin = targetDir == StorageUtils.TRASH_PATH_PLACEHOLDER - val result: FieldMap = hashMapOf( - "uri" to sourceUri.toString(), - "success" to false, - ) - - if (sourcePath != null) { - // on API 30 we cannot get access granted directly to a volume root from its document tree, - // but it is still less constraining to use tree document files than to rely on the Media Store - // - // Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but: - // - we need to scan the file to get the Media Store content URI - // - the underlying document provider controls the new file name - // - // Relying on the Media Store, we can create an item via `ContentResolver.insert()` - // with a path, and retrieve its content URI, but: - // - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`) - // - the volume name should be lower case, not exactly as the `StorageVolume` UUID - // cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()` - // - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?) - // - there is no documentation regarding support for usage with removable storage - // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage - try { - val newFields = if (isCancelledOp()) skippedFieldMap else moveSingle( - activity = activity, - sourcePath = sourcePath, - sourceUri = sourceUri, - targetDir = targetDir, - targetDirDocFile = targetDirDocFile, - nameConflictStrategy = nameConflictStrategy, - mimeType = mimeType, - copy = copy, - ) - result["newFields"] = newFields - result["success"] = true - } catch (e: Exception) { - Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e) + var effectiveTargetDir: String? = null + var targetDirDocFile: DocumentFileCompat? = null + if (!toBin) { + effectiveTargetDir = targetDir + targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) + if (!File(targetDir).exists()) { + callback.onFailure(Exception("failed to create directory at path=$targetDir")) + return } } - callback.onSuccess(result) + + for (entry in entries) { + val mimeType = entry.mimeType + val trashed = entry.trashed + + val sourceUri = if (trashed) Uri.fromFile(File(entry.trashPath!!)) else entry.uri + val sourcePath = if (trashed) entry.trashPath else entry.path + + var desiredName: String? = null + if (trashed) { + entry.path?.let { desiredName = File(it).name } + } + + val result: FieldMap = hashMapOf( + // `uri` should reference original content URI, + // so it is different with `sourceUri` when recycling trashed entries + "uri" to entry.uri.toString(), + "success" to false, + ) + + if (sourcePath != null) { + // on API 30 we cannot get access granted directly to a volume root from its document tree, + // but it is still less constraining to use tree document files than to rely on the Media Store + // + // Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but: + // - we need to scan the file to get the Media Store content URI + // - the underlying document provider controls the new file name + // + // Relying on the Media Store, we can create an item via `ContentResolver.insert()` + // with a path, and retrieve its content URI, but: + // - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`) + // - the volume name should be lower case, not exactly as the `StorageVolume` UUID + // cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()` + // - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?) + // - there is no documentation regarding support for usage with removable storage + // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage + try { + if (toBin) { + val trashDir = StorageUtils.trashDirFor(activity, sourcePath) + if (trashDir != null) { + effectiveTargetDir = ensureTrailingSeparator(trashDir.path) + targetDirDocFile = DocumentFileCompat.fromFile(trashDir) + } + } + if (effectiveTargetDir != null) { + val newFields = if (isCancelledOp()) skippedFieldMap else { + val sourceFile = File(sourcePath) + moveSingle( + activity = activity, + sourceFile = sourceFile, + sourceUri = sourceUri, + targetDir = effectiveTargetDir, + targetDirDocFile = targetDirDocFile, + desiredName = desiredName ?: sourceFile.name, + nameConflictStrategy = nameConflictStrategy, + mimeType = mimeType, + copy = copy, + toBin = toBin, + ) + } + result["newFields"] = newFields + result["success"] = true + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e) + } + } + callback.onSuccess(result) + } } } private suspend fun moveSingle( activity: Activity, - sourcePath: String, + sourceFile: File, sourceUri: Uri, targetDir: String, targetDirDocFile: DocumentFileCompat?, + desiredName: String, nameConflictStrategy: NameConflictStrategy, mimeType: String, copy: Boolean, + toBin: Boolean, ): FieldMap { - val sourceFile = File(sourcePath) - val sourceDir = sourceFile.parent?.let { StorageUtils.ensureTrailingSeparator(it) } + val sourcePath = sourceFile.path + val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) } if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) { // nothing to do unless it's a renamed copy return skippedFieldMap } - val sourceFileName = sourceFile.name - val desiredNameWithoutExtension = sourceFileName.replaceFirst(FILE_EXTENSION_PATTERN, "") + val desiredNameWithoutExtension = desiredName.replaceFirst(FILE_EXTENSION_PATTERN, "") val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( activity = activity, dir = targetDir, @@ -432,7 +505,12 @@ class MediaStoreImageProvider : ImageProvider() { Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) } } - + if (toBin) { + return hashMapOf( + "trashed" to true, + "trashPath" to targetPath, + ) + } return scanNewPath(activity, targetPath, mimeType) } @@ -493,10 +571,7 @@ class MediaStoreImageProvider : ImageProvider() { } private fun isDownloadDir(context: Context, dirPath: String): Boolean { - var relativeDir = PathSegments(context, dirPath).relativeDir ?: "" - if (relativeDir.endsWith(File.separator)) { - relativeDir = relativeDir.substring(0, relativeDir.length - 1) - } + val relativeDir = removeTrailingSeparator(PathSegments(context, dirPath).relativeDir ?: "") return relativeDir == Environment.DIRECTORY_DOWNLOADS } 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 51d83b4a3..15d64b552 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 @@ -6,6 +6,7 @@ object MimeTypes { const val ANY = "*/*" // generic raster + private const val AVIF = "image/avif" const val BMP = "image/bmp" private const val DJVU = "image/vnd.djvu" const val GIF = "image/gif" @@ -49,7 +50,7 @@ object MimeTypes { private const val AVI_VND = "video/vnd.avi" const val DVD = "video/dvd" private const val MKV = "video/x-matroska" - private const val MOV = "video/quicktime" + const val MOV = "video/quicktime" private const val MP2T = "video/mp2t" private const val MP2TS = "video/mp2ts" const val MP4 = "video/mp4" @@ -72,7 +73,7 @@ object MimeTypes { // returns whether the specified MIME type represents // a raster image format that allows an alpha channel fun canHaveAlpha(mimeType: String?) = when (mimeType) { - BMP, GIF, ICO, PNG, SVG, TIFF, WEBP -> true + AVIF, BMP, GIF, ICO, PNG, SVG, TIFF, WEBP -> true else -> false } @@ -150,6 +151,7 @@ object MimeTypes { fun extensionFor(mimeType: String): String? = when (mimeType) { ARW -> ".arw" AVI, AVI_VND -> ".avi" + AVIF -> ".avif" BMP -> ".bmp" CR2 -> ".cr2" CRW -> ".crw" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt index 02eeaf485..1e1a70203 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt @@ -18,9 +18,7 @@ import deckers.thibault.aves.PendingStorageAccessResultHandler import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.StorageUtils.PathSegments import java.io.File -import java.util.* import java.util.concurrent.CompletableFuture -import kotlin.collections.ArrayList object PermissionManager { private val LOG_TAG = LogUtils.createTag() @@ -94,11 +92,12 @@ object PermissionManager { } fun getInaccessibleDirectories(context: Context, dirPaths: List): List> { + val concreteDirPaths = dirPaths.filter { it != StorageUtils.TRASH_PATH_PLACEHOLDER } val accessibleDirs = getAccessibleDirs(context) // find set of inaccessible directories for each volume val dirsPerVolume = HashMap>() - for (dirPath in dirPaths.map { if (it.endsWith(File.separator)) it else it + File.separator }) { + for (dirPath in concreteDirPaths.map { if (it.endsWith(File.separator)) it else it + File.separator }) { if (accessibleDirs.none { dirPath.startsWith(it) }) { // inaccessible dirs val segments = PathSegments(context, dirPath) @@ -211,7 +210,8 @@ object PermissionManager { ) }) } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT - || Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT_WATCH) { + || Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT_WATCH + ) { // removable storage requires access permission, at the file level // without directory access, we consider the whole volume restricted val primaryVolume = StorageUtils.getPrimaryVolumePath(context) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index bc9f217f2..a420fb1f3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -33,6 +33,32 @@ object StorageUtils { private const val TREE_URI_ROOT = "content://com.android.externalstorage.documents/tree/" private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)") + const val TRASH_PATH_PLACEHOLDER = "#trash" + + private fun isAppFile(context: Context, path: String): Boolean { + return context.getExternalFilesDirs(null).any { filesDir -> path.startsWith(filesDir.path) } + } + + private fun appExternalFilesDirFor(context: Context, path: String): File? { + val filesDirs = context.getExternalFilesDirs(null) + val volumePath = getVolumePath(context, path) + return volumePath?.let { filesDirs.firstOrNull { it.startsWith(volumePath) } } ?: filesDirs.first() + } + + fun trashDirFor(context: Context, path: String): File? { + val filesDir = appExternalFilesDirFor(context, path) + if (filesDir == null) { + Log.e(LOG_TAG, "failed to find external files dir for path=$path") + return null + } + val trashDir = File(filesDir, "trash") + if (!trashDir.exists() && !trashDir.mkdirs()) { + Log.e(LOG_TAG, "failed to create directories at path=$trashDir") + return null + } + return trashDir + } + /** * Volume paths */ @@ -268,10 +294,7 @@ object StorageUtils { fun convertDirPathToTreeUri(context: Context, dirPath: String): Uri? { val uuid = getVolumeUuidForTreeUri(context, dirPath) if (uuid != null) { - var relativeDir = PathSegments(context, dirPath).relativeDir ?: "" - if (relativeDir.endsWith(File.separator)) { - relativeDir = relativeDir.substring(0, relativeDir.length - 1) - } + val relativeDir = removeTrailingSeparator(PathSegments(context, dirPath).relativeDir ?: "") return DocumentsContract.buildTreeDocumentUri("com.android.externalstorage.documents", "$uuid:$relativeDir") } Log.e(LOG_TAG, "failed to convert dirPath=$dirPath to tree URI") @@ -329,7 +352,7 @@ object StorageUtils { // try to strip user info, if any if (mediaUri.userInfo != null) { - val genericMediaUri = Uri.parse(mediaUri.toString().replaceFirst("${mediaUri.userInfo}@", "")) + val genericMediaUri = stripMediaUriUserInfo(mediaUri) Log.d(LOG_TAG, "retry getDocumentFile for mediaUri=$mediaUri without userInfo: $genericMediaUri") return getDocumentFile(context, anyPath, genericMediaUri) } @@ -408,10 +431,11 @@ object StorageUtils { fun canEditByFile(context: Context, path: String) = !requireAccessPermission(context, path) fun requireAccessPermission(context: Context, anyPath: String): Boolean { + if (isAppFile(context, anyPath)) return false + // on Android R, we should always require access permission, even on primary volume - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - return true - } + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true + val onPrimaryVolume = anyPath.startsWith(getPrimaryVolumePath(context)) return !onPrimaryVolume } @@ -442,36 +466,71 @@ object StorageUtils { // As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used // to work around a bug from Android Q where metadata redaction corrupts HEIC images. // This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException` - // for some content URIs (e.g. `content://media/external_primary/downloads/...`) - // so we build a typical `images` or `videos` content URI from the original content ID. - fun getGlideSafeUri(uri: Uri, mimeType: String): Uri = normalizeMediaUri(uri, mimeType) - - // requesting access or writing to some MediaStore content URIs - // e.g. `content://0@media/...`, `content://media/external_primary/downloads/...` - // yields an exception with `All requested items must be referenced by specific ID` - fun getMediaStoreScopedStorageSafeUri(uri: Uri, mimeType: String): Uri = normalizeMediaUri(uri, mimeType) - - private fun normalizeMediaUri(uri: Uri, mimeType: String): Uri { + // for some non image/video content URIs (e.g. `downloads`, `file`) + fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String): Uri { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaStoreContentUri(uri)) { - // we cannot safely apply this to a file content URI, as it may point to a file not indexed - // by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI - if (uri.path?.contains("/downloads/") == true) { - uri.tryParseId()?.let { id -> - return 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 + val uriPath = uri.path + when { + uriPath?.contains("/downloads/") == true -> { + // e.g. `content://media/external_primary/downloads/...` + getMediaUriImageVideoUri(uri, mimeType)?.let { imageVideUri -> return imageVideUri } + } + uriPath?.contains("/file/") == true -> { + // e.g. `content://media/external/file/...` + // create an ad-hoc temporary file for decoding only + File.createTempFile("aves", null).apply { + deleteOnExit() + try { + outputStream().use { output -> + openInputStream(context, uri)?.use { input -> + input.copyTo(output) + } + } + return Uri.fromFile(this) + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to create temporary file from uri=$uri", e) + } } } - } else if (uri.userInfo != null) { - // strip user info, if any - return Uri.parse(uri.toString().replaceFirst("${uri.userInfo}@", "")) + uri.userInfo != null -> return stripMediaUriUserInfo(uri) } - } return uri } + // requesting access or writing to some MediaStore content URIs + // yields an exception with `All requested items must be referenced by specific ID` + fun getMediaStoreScopedStorageSafeUri(uri: Uri, mimeType: String): Uri { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaStoreContentUri(uri)) { + val uriPath = uri.path + when { + uriPath?.contains("/downloads/") == true -> { + // e.g. `content://media/external_primary/downloads/...` + getMediaUriImageVideoUri(uri, mimeType)?.let { imageVideUri -> return imageVideUri } + } + uri.userInfo != null -> return stripMediaUriUserInfo(uri) + } + } + return uri + } + + // Build a typical `images` or `videos` content URI from the original content ID. + // We cannot safely apply this to a `file` content URI, as it may point to a file not indexed + // by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI. + private fun getMediaUriImageVideoUri(uri: Uri, mimeType: String): Uri? { + return uri.tryParseId()?.let { id -> + return 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 + } + } + } + + // strip user info, if any + // e.g. `content://0@media/...` + private fun stripMediaUriUserInfo(uri: Uri) = Uri.parse(uri.toString().replaceFirst("${uri.userInfo}@", "")) + fun openInputStream(context: Context, uri: Uri): InputStream? { val effectiveUri = getOriginalUri(context, uri) return try { @@ -517,6 +576,10 @@ object StorageUtils { return if (dirPath.endsWith(File.separator)) dirPath else dirPath + File.separator } + fun removeTrailingSeparator(dirPath: String): String { + return if (dirPath.endsWith(File.separator)) dirPath.substring(0, dirPath.length - 1) else dirPath + } + // `fullPath` should match "volumePath + relativeDir + fileName" class PathSegments(context: Context, fullPath: String) { var volumePath: String? = null // `volumePath` with trailing "/" diff --git a/android/app/src/main/res/values-id/strings.xml b/android/app/src/main/res/values-id/strings.xml new file mode 100644 index 000000000..f750d1a0f --- /dev/null +++ b/android/app/src/main/res/values-id/strings.xml @@ -0,0 +1,10 @@ + + + Aves + Cari + Video + Pindai media + Pindai gambar & video + Memindai media + Berhenti + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 57fd9118c..4174edca7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.0' + classpath 'com.android.tools.build:gradle:7.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // GMS & Firebase Crashlytics are not actually used by all flavors classpath 'com.google.gms:google-services:4.3.10' diff --git a/fastlane/metadata/android/en-US/changelogs/1066.txt b/fastlane/metadata/android/en-US/changelogs/1066.txt new file mode 100644 index 000000000..f1cc67dc3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1066.txt @@ -0,0 +1,5 @@ +In v1.6.0: +- recycle bin +- view small and large images at their actual size +- enjoy the app in Indonesian +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt new file mode 100644 index 000000000..60d4c271f --- /dev/null +++ b/fastlane/metadata/android/id/full_description.txt @@ -0,0 +1,5 @@ +Aves dapat menangani semua jenis gambar dan video, termasuk JPEG dan MP4, tetapi juga hal-hal yang lebih eksotis seperti TIFF halaman-multi, SVG, AVI lama, dan lainnya! Ini memindai koleksi media Anda untuk mengidentifikasi foto bergerak, panorama (foto 360), video 360°, dan file GeoTIFF. + +Navigasi dan pencarian merupakan bagian penting dari Aves. Tujuannya adalah agar pengguna dengan mudah mengalir dari album ke foto ke tag ke peta, dll. + +Aves terintegrasi dengan Android (dari API 19 ke 32, yaitu dari KitKat ke Android 12L) dengan fitur-fitur seperti pintasan aplikasi dan pencarian global penanganan. Ini juga berfungsi sebagai penampil dan pemilih media. \ No newline at end of file diff --git a/fastlane/metadata/android/id/images/featureGraphic.png b/fastlane/metadata/android/id/images/featureGraphic.png new file mode 100644 index 000000000..11d0ca7af Binary files /dev/null and b/fastlane/metadata/android/id/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/1.png b/fastlane/metadata/android/id/images/phoneScreenshots/1.png new file mode 100644 index 000000000..9bca9f58b Binary files /dev/null and b/fastlane/metadata/android/id/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/2.png b/fastlane/metadata/android/id/images/phoneScreenshots/2.png new file mode 100644 index 000000000..85100c774 Binary files /dev/null and b/fastlane/metadata/android/id/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/3.png b/fastlane/metadata/android/id/images/phoneScreenshots/3.png new file mode 100644 index 000000000..cfdeeec8f Binary files /dev/null and b/fastlane/metadata/android/id/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/4.png b/fastlane/metadata/android/id/images/phoneScreenshots/4.png new file mode 100644 index 000000000..d5240f2b0 Binary files /dev/null and b/fastlane/metadata/android/id/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/5.png b/fastlane/metadata/android/id/images/phoneScreenshots/5.png new file mode 100644 index 000000000..00baaa099 Binary files /dev/null and b/fastlane/metadata/android/id/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/6.png b/fastlane/metadata/android/id/images/phoneScreenshots/6.png new file mode 100644 index 000000000..be711d86a Binary files /dev/null and b/fastlane/metadata/android/id/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt new file mode 100644 index 000000000..0dc05cdc9 --- /dev/null +++ b/fastlane/metadata/android/id/short_description.txt @@ -0,0 +1 @@ +Galeri dan penjelajah metadata \ No newline at end of file diff --git a/lib/app_mode.dart b/lib/app_mode.dart index b93ec33e6..34f38a48b 100644 --- a/lib/app_mode.dart +++ b/lib/app_mode.dart @@ -1,11 +1,11 @@ -enum AppMode { main, pickExternal, pickInternal, view } +enum AppMode { main, pickMediaExternal, pickMediaInternal, pickFilterInternal, view } extension ExtraAppMode on AppMode { - bool get canSearch => this == AppMode.main || this == AppMode.pickExternal; + bool get canSearch => this == AppMode.main || this == AppMode.pickMediaExternal; bool get canSelect => this == AppMode.main; - bool get hasDrawer => this == AppMode.main || this == AppMode.pickExternal; + bool get hasDrawer => this == AppMode.main || this == AppMode.pickMediaExternal; - bool get isPicking => this == AppMode.pickExternal || this == AppMode.pickInternal; + bool get isPickingMedia => this == AppMode.pickMediaExternal || this == AppMode.pickMediaInternal; } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 438c05eca..a74c025cd 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, =1{1 Sekunde} other{{seconds} Sekunde}}", "timeMinutes": "{minutes, plural, =1{1 Minute} other{{minutes} Minuten}}", + "timeDays": "{days, plural, =1{1 Tag} other{{days} Tage}}", "focalLength": "{length} mm", "applyButtonLabel": "ANWENDEN", @@ -27,6 +28,7 @@ "resetButtonTooltip": "Zurücksetzen", "doubleBackExitMessage": "Zum Verlassen erneut auf „Zurück“ tippen.", + "doNotAskAgain": "Nicht noch einmal fragen", "sourceStateLoading": "Laden", "sourceStateCataloguing": "Katalogisierung", @@ -47,8 +49,8 @@ "entryActionCopyToClipboard": "In die Zwischenablage kopieren", "entryActionDelete": "Löschen", "entryActionExport": "Exportieren", - "entryActionInfo": "Info", "entryActionRename": "Umbenennen", + "entryActionRestore": "Wiederherstellen", "entryActionRotateCCW": "Drehen gegen den Uhrzeigersinn", "entryActionRotateCW": "Drehen im Uhrzeigersinn", "entryActionFlip": "Horizontal spiegeln", @@ -56,10 +58,10 @@ "entryActionShare": "Teilen", "entryActionViewSource": "Quelle anzeigen", "entryActionViewMotionPhotoVideo": "Bewegtes Foto öffnen", - "entryActionEdit": "Bearbeiten mit...", - "entryActionOpen": "Öffnen mit...", - "entryActionSetAs": "Einstellen als...", - "entryActionOpenMap": "In der Karten-App anzeigen...", + "entryActionEdit": "Bearbeiten", + "entryActionOpen": "Öffnen mit", + "entryActionSetAs": "Einstellen als", + "entryActionOpenMap": "In der Karten-App anzeigen", "entryActionRotateScreen": "Bildschirm rotieren", "entryActionAddFavourite": "Zu Favoriten hinzufügen ", "entryActionRemoveFavourite": "Aus Favoriten entfernen", @@ -79,6 +81,7 @@ "entryInfoActionEditTags": "Tags bearbeiten", "entryInfoActionRemoveMetadata": "Metadaten entfernen", + "filterBinLabel": "Papierkorb", "filterFavouriteLabel": "Favorit", "filterLocationEmptyLabel": "Ungeortet", "filterTagEmptyLabel": "Unmarkiert", @@ -157,6 +160,7 @@ "noMatchingAppDialogTitle": "Keine passende App", "noMatchingAppDialogMessage": "Es gibt keine Anwendungen, die dies bewältigen können.", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Dieses Element in den Papierkorb verschieben?} other{Diese {count} Elemente in den Papierkorb verschieben?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Sicher, dass dieses Element gelöscht werden soll?} other{Sicher, dass diese {count} Elemente gelöscht werden sollen?}}", "videoResumeDialogMessage": "Soll bei {time} weiter abspielt werden?", @@ -267,11 +271,12 @@ "collectionPageTitle": "Sammlung", "collectionPickPageTitle": "Wähle", - "collectionSelectionPageTitle": "{count, plural, =0{Elemente auswählen} =1{1 Element} other{{count} Elemente}}", + "collectionSelectPageTitle": "Elemente auswählen", "collectionActionShowTitleSearch": "Titelfilter anzeigen", "collectionActionHideTitleSearch": "Titelfilter ausblenden", "collectionActionAddShortcut": "Verknüpfung hinzufügen", + "collectionActionEmptyBin": "Papierkorb leeren", "collectionActionCopy": "In Album kopieren", "collectionActionMove": "Zum Album verschieben", "collectionActionRescan": "Neu scannen", @@ -350,6 +355,8 @@ "tagPageTitle": "Tags", "tagEmpty": "Keine Tags", + "binPageTitle": "Papierkorb", + "searchCollectionFieldHint": "Sammlung durchsuchen", "searchSectionRecent": "Neueste", "searchSectionAlbums": "Alben", @@ -375,6 +382,11 @@ "settingsKeepScreenOnTitle": "Bildschirm eingeschaltet lassen", "settingsDoubleBackExit": "Zum Verlassen zweimal „zurück“ tippen", + "settingsConfirmationDialogTile": "Bestätigungsdialoge", + "settingsConfirmationDialogTitle": "Bestätigungsdialoge", + "settingsConfirmationDialogDeleteItems": "Vor dem endgültigen Löschen von Elementen fragen", + "settingsConfirmationDialogMoveToBinItems": "Vor dem Verschieben von Elementen in den Papierkorb fragen", + "settingsNavigationDrawerTile": "Menü Navigation", "settingsNavigationDrawerEditorTitle": "Menü Navigation", "settingsNavigationDrawerBanner": "Die Taste berühren und halten, um Menüpunkte zu verschieben und neu anzuordnen.", @@ -450,6 +462,8 @@ "settingsAllowInstalledAppAccessSubtitle": "zur Gruppierung von Bildern nach Apps", "settingsAllowErrorReporting": "Anonyme Fehlermeldungen zulassen", "settingsSaveSearchHistory": "Suchverlauf speichern", + "settingsEnableBin": "Papierkorb verwenden", + "settingsEnableBinSubtitle": "Gelöschte Elemente 30 Tage lang aufbewahren", "settingsHiddenItemsTile": "Versteckte Elemente", "settingsHiddenItemsTitle": "Versteckte Elemente", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0407e89cb..37598546e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -22,6 +22,12 @@ "minutes": {} } }, + "timeDays": "{days, plural, =1{1 day} other{{days} days}}", + "@timeDays": { + "placeholders": { + "days": {} + } + }, "focalLength": "{length} mm", "@focalLength": { "placeholders": { @@ -50,6 +56,7 @@ "resetButtonTooltip": "Reset", "doubleBackExitMessage": "Tap “back” again to exit.", + "doNotAskAgain": "Do not ask again", "sourceStateLoading": "Loading", "sourceStateCataloguing": "Cataloguing", @@ -69,9 +76,10 @@ "entryActionCopyToClipboard": "Copy to clipboard", "entryActionDelete": "Delete", + "entryActionConvert": "Convert", "entryActionExport": "Export", - "entryActionInfo": "Info", "entryActionRename": "Rename", + "entryActionRestore": "Restore", "entryActionRotateCCW": "Rotate counterclockwise", "entryActionRotateCW": "Rotate clockwise", "entryActionFlip": "Flip horizontally", @@ -79,10 +87,10 @@ "entryActionShare": "Share", "entryActionViewSource": "View source", "entryActionViewMotionPhotoVideo": "Open Motion Photo", - "entryActionEdit": "Edit with…", - "entryActionOpen": "Open with…", - "entryActionSetAs": "Set as…", - "entryActionOpenMap": "Show in map app…", + "entryActionEdit": "Edit", + "entryActionOpen": "Open with", + "entryActionSetAs": "Set as", + "entryActionOpenMap": "Show in map app", "entryActionRotateScreen": "Rotate screen", "entryActionAddFavourite": "Add to favourites", "entryActionRemoveFavourite": "Remove from favourites", @@ -102,6 +110,7 @@ "entryInfoActionEditTags": "Edit tags", "entryInfoActionRemoveMetadata": "Remove metadata", + "filterBinLabel": "Recycle bin", "filterFavouriteLabel": "Favourite", "filterLocationEmptyLabel": "Unlocated", "filterTagEmptyLabel": "Untagged", @@ -254,7 +263,13 @@ "noMatchingAppDialogTitle": "No Matching App", "noMatchingAppDialogMessage": "There are no apps that can handle this.", - "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this item?} other{Are you sure you want to delete these {count} items?}}", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Move this item to the recycle bin?} other{Move these {count} items to the recycle bin?}}", + "@binEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Delete this item?} other{Delete these {count} items?}}", "@deleteEntriesConfirmationDialogMessage": { "placeholders": { "count": {} @@ -287,13 +302,13 @@ "renameAlbumDialogLabel": "New name", "renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists", - "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this album and its item?} other{Are you sure you want to delete this album and its {count} items?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and its item?} other{Delete this album and its {count} items?}}", "@deleteSingleAlbumConfirmationDialogMessage": { "placeholders": { "count": {} } }, - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete these albums and their item?} other{Are you sure you want to delete these albums and their {count} items?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Delete these albums and their item?} other{Delete these albums and their {count} items?}}", "@deleteMultiAlbumConfirmationDialogMessage": { "placeholders": { "count": {} @@ -399,16 +414,12 @@ "collectionPageTitle": "Collection", "collectionPickPageTitle": "Pick", - "collectionSelectionPageTitle": "{count, plural, =0{Select items} =1{1 item} other{{count} items}}", - "@collectionSelectionPageTitle": { - "placeholders": { - "count": {} - } - }, + "collectionSelectPageTitle": "Select items", "collectionActionShowTitleSearch": "Show title filter", "collectionActionHideTitleSearch": "Hide title filter", "collectionActionAddShortcut": "Add shortcut", + "collectionActionEmptyBin": "Empty bin", "collectionActionCopy": "Copy to album", "collectionActionMove": "Move to album", "collectionActionRescan": "Rescan", @@ -527,6 +538,8 @@ "tagPageTitle": "Tags", "tagEmpty": "No tags", + "binPageTitle": "Recycle Bin", + "searchCollectionFieldHint": "Search collection", "searchSectionRecent": "Recent", "searchSectionAlbums": "Albums", @@ -552,6 +565,11 @@ "settingsKeepScreenOnTitle": "Keep Screen On", "settingsDoubleBackExit": "Tap “back” twice to exit", + "settingsConfirmationDialogTile": "Confirmation dialogs", + "settingsConfirmationDialogTitle": "Confirmation Dialogs", + "settingsConfirmationDialogDeleteItems": "Ask before deleting items forever", + "settingsConfirmationDialogMoveToBinItems": "Ask before moving items to the recycle bin", + "settingsNavigationDrawerTile": "Navigation menu", "settingsNavigationDrawerEditorTitle": "Navigation Menu", "settingsNavigationDrawerBanner": "Touch and hold to move and reorder menu items.", @@ -627,6 +645,8 @@ "settingsAllowInstalledAppAccessSubtitle": "Used to improve album display", "settingsAllowErrorReporting": "Allow anonymous error reporting", "settingsSaveSearchHistory": "Save search history", + "settingsEnableBin": "Use recycle bin", + "settingsEnableBinSubtitle": "Keep deleted items for 30 days", "settingsHiddenItemsTile": "Hidden items", "settingsHiddenItemsTitle": "Hidden Items", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 232abb8d3..39ac08f98 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}", "timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}", + "timeDays": "{days, plural, =1{1 día} other{{days} días}}", "focalLength": "{length} mm", "applyButtonLabel": "APLICAR", @@ -27,6 +28,7 @@ "resetButtonTooltip": "Restablecer", "doubleBackExitMessage": "Presione «atrás» nuevamente para salir.", + "doNotAskAgain": "No preguntar nuevamente", "sourceStateLoading": "Cargando", "sourceStateCataloguing": "Catalogando", @@ -46,9 +48,10 @@ "entryActionCopyToClipboard": "Copiar al portapapeles", "entryActionDelete": "Borrar", + "entryActionConvert": "Convertir", "entryActionExport": "Exportar", - "entryActionInfo": "Información", "entryActionRename": "Renombrar", + "entryActionRestore": "Restaurar", "entryActionRotateCCW": "Rotar en sentido antihorario", "entryActionRotateCW": "Rotar en sentido horario", "entryActionFlip": "Voltear horizontalmente", @@ -56,10 +59,10 @@ "entryActionShare": "Compartir", "entryActionViewSource": "Ver fuente", "entryActionViewMotionPhotoVideo": "Abrir foto en movimiento", - "entryActionEdit": "Editar con…", - "entryActionOpen": "Abrir con…", - "entryActionSetAs": "Establecer como…", - "entryActionOpenMap": "Mostrar en aplicación de mapa…", + "entryActionEdit": "Editar", + "entryActionOpen": "Abrir con", + "entryActionSetAs": "Establecer como", + "entryActionOpenMap": "Mostrar en aplicación de mapa", "entryActionRotateScreen": "Rotar pantalla", "entryActionAddFavourite": "Agregar a favoritos", "entryActionRemoveFavourite": "Quitar de favoritos", @@ -74,10 +77,12 @@ "videoActionSettings": "Ajustes", "entryInfoActionEditDate": "Editar fecha y hora", + "entryInfoActionEditLocation": "Editar ubicación", "entryInfoActionEditRating": "Editar clasificación", "entryInfoActionEditTags": "Editar etiquetas", "entryInfoActionRemoveMetadata": "Eliminar metadatos", + "filterBinLabel": "Cesto de basura", "filterFavouriteLabel": "Favorito", "filterLocationEmptyLabel": "No localizado", "filterTagEmptyLabel": "Sin etiquetar", @@ -157,6 +162,8 @@ "noMatchingAppDialogTitle": "Sin aplicación compatible", "noMatchingAppDialogMessage": "No se encontraron aplicaciones para manejar esto.", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{¿Mover este elemento al cesto de basura?} other{¿Mover estos {count} elementos al cesto de basura?}}", + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de borrar este elemento?} other{¿Está seguro de querer borrar {count} elementos?}}", "videoResumeDialogMessage": "¿Desea reanudar la reproducción a las {time}?", @@ -181,6 +188,8 @@ "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de que desea borrar estos álbumes y un elemento?} other{¿Está seguro de que desea borrar estos álbumes y sus {count} elementos?}}", "exportEntryDialogFormat": "Formato:", + "exportEntryDialogWidth": "Anchura", + "exportEntryDialogHeight": "Altura", "renameEntryDialogLabel": "Renombrar", @@ -194,6 +203,13 @@ "editEntryDateDialogHours": "Horas", "editEntryDateDialogMinutes": "Minutos", + "editEntryLocationDialogTitle": "Ubicación", + "editEntryLocationDialogChooseOnMapTooltip": "Elegir en el mapa", + "editEntryLocationDialogLatitude": "Latitud", + "editEntryLocationDialogLongitude": "Longitud", + + "locationPickerUseThisLocationButton": "Usar esta ubicación", + "editEntryRatingDialogTitle": "Clasificación", "removeEntryMetadataDialogTitle": "Eliminación de metadatos", @@ -258,11 +274,12 @@ "collectionPageTitle": "Colección", "collectionPickPageTitle": "Elegir", - "collectionSelectionPageTitle": "{count, plural, =0{Seleccionar} =1{1 elemento} other{{count} elementos}}", + "collectionSelectPageTitle": "Seleccionar", "collectionActionShowTitleSearch": "Mostrar filtros de títulos", "collectionActionHideTitleSearch": "Ocultar filtros de títulos", "collectionActionAddShortcut": "Agregar atajo", + "collectionActionEmptyBin": "Vaciar cesto", "collectionActionCopy": "Copiar a álbum", "collectionActionMove": "Mover a álbum", "collectionActionRescan": "Volver a buscar", @@ -341,6 +358,8 @@ "tagPageTitle": "Etiquetas", "tagEmpty": "Sin etiquetas", + "binPageTitle": "Cesto de basura", + "searchCollectionFieldHint": "Buscar en colección", "searchSectionRecent": "Reciente", "searchSectionAlbums": "Álbumes", @@ -366,6 +385,11 @@ "settingsKeepScreenOnTitle": "Mantener pantalla encendida", "settingsDoubleBackExit": "Presione «atrás» dos veces para salir", + "settingsConfirmationDialogTile": "Diálogos de confirmación", + "settingsConfirmationDialogTitle": "Diálogos de confirmación", + "settingsConfirmationDialogDeleteItems": "Preguntar antes de eliminar elementos permanentemente", + "settingsConfirmationDialogMoveToBinItems": "Preguntar antes de mover elementos al cesto de basura", + "settingsNavigationDrawerTile": "Menú de navegación", "settingsNavigationDrawerEditorTitle": "Menú de navegación", "settingsNavigationDrawerBanner": "Toque y mantenga para mover y reordenar elementos del menú.", @@ -441,6 +465,8 @@ "settingsAllowInstalledAppAccessSubtitle": "Usado para mejorar los álbumes mostrados", "settingsAllowErrorReporting": "Permitir reporte de errores anónimo", "settingsSaveSearchHistory": "Guardar historial de búsqueda", + "settingsEnableBin": "Usar cesto de basura", + "settingsEnableBinSubtitle": "Guardar los elementos eliminados por 30 días", "settingsHiddenItemsTile": "Elementos ocultos", "settingsHiddenItemsTitle": "Elementos ocultos", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 115812126..65759fb4e 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -5,8 +5,9 @@ "welcomeTermsToggle": "J’accepte les conditions d’utilisation", "itemCount": "{count, plural, =1{1 élément} other{{count} éléments}}", - "timeSeconds": "{seconds, plural, =1{1 seconde} other{{seconds} secondes}}", - "timeMinutes": "{minutes, plural, =1{1 minute} other{{minutes} minutes}}", + "timeSeconds": "{seconds, plural, =0{0 seconde} =1{1 seconde} other{{seconds} secondes}}", + "timeMinutes": "{minutes, plural, =0{0 minute} =1{1 minute} other{{minutes} minutes}}", + "timeDays": "{days, plural, =0{0 jour} =1{1 jour} other{{days} jours}}", "focalLength": "{length} mm", "applyButtonLabel": "ENREGISTRER", @@ -27,6 +28,7 @@ "resetButtonTooltip": "Réinitialiser", "doubleBackExitMessage": "Pressez «\u00A0retour\u00A0» à nouveau pour quitter.", + "doNotAskAgain": "Ne pas demander de nouveau", "sourceStateLoading": "Chargement", "sourceStateCataloguing": "Classification", @@ -46,9 +48,10 @@ "entryActionCopyToClipboard": "Copier dans presse-papier", "entryActionDelete": "Supprimer", + "entryActionConvert": "Convertir", "entryActionExport": "Exporter", - "entryActionInfo": "Détails", "entryActionRename": "Renommer", + "entryActionRestore": "Restaurer", "entryActionRotateCCW": "Pivoter à gauche", "entryActionRotateCW": "Pivoter à droite", "entryActionFlip": "Retourner horizontalement", @@ -56,10 +59,10 @@ "entryActionShare": "Partager", "entryActionViewSource": "Voir le code", "entryActionViewMotionPhotoVideo": "Ouvrir le clip vidéo", - "entryActionEdit": "Modifier avec…", - "entryActionOpen": "Ouvrir avec…", - "entryActionSetAs": "Utiliser comme…", - "entryActionOpenMap": "Localiser avec…", + "entryActionEdit": "Modifier", + "entryActionOpen": "Ouvrir avec", + "entryActionSetAs": "Utiliser comme", + "entryActionOpenMap": "Localiser avec", "entryActionRotateScreen": "Pivoter l’écran", "entryActionAddFavourite": "Ajouter aux favoris", "entryActionRemoveFavourite": "Retirer des favoris", @@ -79,6 +82,7 @@ "entryInfoActionEditTags": "Modifier les libellés", "entryInfoActionRemoveMetadata": "Retirer les métadonnées", + "filterBinLabel": "Corbeille", "filterFavouriteLabel": "Favori", "filterLocationEmptyLabel": "Sans lieu", "filterTagEmptyLabel": "Sans libellé", @@ -157,7 +161,8 @@ "noMatchingAppDialogTitle": "App indisponible", "noMatchingAppDialogMessage": "Aucune app ne peut effectuer cette opération.", - "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Voulez-vous vraiment supprimer cet élément ?} other{Voulez-vous vraiment supprimer ces {count} éléments ?}}", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Mettre cet élément à la corbeille ?} other{Mettre ces {count} éléments à la corbeille ?}}", + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Supprimer cet élément ?} other{Supprimer ces {count} éléments ?}}", "videoResumeDialogMessage": "Voulez-vous reprendre la lecture à {time} ?", "videoStartOverButtonLabel": "RECOMMENCER", @@ -177,8 +182,8 @@ "renameAlbumDialogLabel": "Nouveau nom", "renameAlbumDialogLabelAlreadyExistsHelper": "Le dossier existe déjà", - "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Voulez-vous vraiment supprimer cet album et son élément ?} other{Voulez-vous vraiment supprimer cet album et ses {count} éléments ?}}", - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Voulez-vous vraiment supprimer ces albums et leur élément ?} other{Voulez-vous vraiment supprimer ces albums et leurs {count} éléments ?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer cet album et son élément ?} other{Supprimer cet album et ses {count} éléments ?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer ces albums et leur élément ?} other{Supprimer ces albums et leurs {count} éléments ?}}", "exportEntryDialogFormat": "Format :", "exportEntryDialogWidth": "Largeur", @@ -188,7 +193,7 @@ "editEntryDateDialogTitle": "Date & Heure", "editEntryDateDialogSetCustom": "Régler une date personnalisée", - "editEntryDateDialogCopyField": "Copier d'une autre date", + "editEntryDateDialogCopyField": "Copier d’une autre date", "editEntryDateDialogExtractFromTitle": "Extraire du titre", "editEntryDateDialogShift": "Décaler", "editEntryDateDialogSourceFileModifiedDate": "Date de modification du fichier", @@ -267,11 +272,12 @@ "collectionPageTitle": "Collection", "collectionPickPageTitle": "Sélection", - "collectionSelectionPageTitle": "{count, plural, =0{Sélection} =1{1 élément} other{{count} éléments}}", + "collectionSelectPageTitle": "Sélection", "collectionActionShowTitleSearch": "Filtrer les titres", "collectionActionHideTitleSearch": "Masquer le filtre", "collectionActionAddShortcut": "Créer un raccourci", + "collectionActionEmptyBin": "Vider la corbeille", "collectionActionCopy": "Copier vers l’album", "collectionActionMove": "Déplacer vers l’album", "collectionActionRescan": "Réanalyser", @@ -350,6 +356,8 @@ "tagPageTitle": "Libellés", "tagEmpty": "Aucun libellé", + "binPageTitle": "Corbeille", + "searchCollectionFieldHint": "Recherche", "searchSectionRecent": "Historique", "searchSectionAlbums": "Albums", @@ -375,6 +383,11 @@ "settingsKeepScreenOnTitle": "Allumage de l’écran", "settingsDoubleBackExit": "Presser «\u00A0retour\u00A0» 2 fois pour quitter", + "settingsConfirmationDialogTile": "Demandes de confirmation", + "settingsConfirmationDialogTitle": "Demandes de confirmation", + "settingsConfirmationDialogDeleteItems": "Suppression définitive d’éléments", + "settingsConfirmationDialogMoveToBinItems": "Mise d’éléments à la corbeille", + "settingsNavigationDrawerTile": "Menu de navigation", "settingsNavigationDrawerEditorTitle": "Menu de navigation", "settingsNavigationDrawerBanner": "Maintenez votre doigt appuyé pour déplacer et réorganiser les éléments de menu.", @@ -450,6 +463,8 @@ "settingsAllowInstalledAppAccessSubtitle": "Pour un affichage amélioré des albums", "settingsAllowErrorReporting": "Autoriser l’envoi de rapports d’erreur", "settingsSaveSearchHistory": "Maintenir un historique des recherches", + "settingsEnableBin": "Utiliser la corbeille", + "settingsEnableBinSubtitle": "Conserver les éléments supprimés pendant 30 jours", "settingsHiddenItemsTile": "Éléments masqués", "settingsHiddenItemsTitle": "Éléments masqués", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb new file mode 100644 index 000000000..468e6b2ba --- /dev/null +++ b/lib/l10n/app_id.arb @@ -0,0 +1,560 @@ +{ + "appName": "Aves", + "welcomeMessage": "Selamat datang ke Aves", + "welcomeOptional": "Opsional", + "welcomeTermsToggle": "Saya menyetujui syarat dan ketentuan", + "itemCount": "{count, plural, other{{count} benda}}", + + "timeSeconds": "{seconds, plural, other{{seconds} detik}}", + "timeMinutes": "{minutes, plural, other{{minutes} menit}}", + "timeDays": "{days, plural, other{{days} hari}}", + "focalLength": "{length} mm", + + "applyButtonLabel": "TERAPKAN", + "deleteButtonLabel": "BUANG", + "nextButtonLabel": "SELANJUTNYA", + "showButtonLabel": "TAMPILKAN", + "hideButtonLabel": "SEMBUNYIKAN", + "continueButtonLabel": "SELANJUTNYA", + + "cancelTooltip": "Batalkan", + "changeTooltip": "Ganti", + "clearTooltip": "Hapus", + "previousTooltip": "Sebelumnya", + "nextTooltip": "Selanjutnya", + "showTooltip": "Tampilkan", + "hideTooltip": "Sembunyikan", + "actionRemove": "Hapus", + "resetButtonTooltip": "Ulang", + + "doubleBackExitMessage": "Ketuk “kembali” lagi untuk keluar.", + "doNotAskAgain": "Jangan tanya lagi", + + "sourceStateLoading": "Memuat", + "sourceStateCataloguing": "Mengkatalog", + "sourceStateLocatingCountries": "Mencari negara", + "sourceStateLocatingPlaces": "Mencari tempat", + + "chipActionDelete": "Hapus", + "chipActionGoToAlbumPage": "Tampilkan di Album", + "chipActionGoToCountryPage": "Tampilkan di Negara", + "chipActionGoToTagPage": "Tampilkan di Tag", + "chipActionHide": "Sembunyikan", + "chipActionPin": "Sematkan ke atas", + "chipActionUnpin": "Lepas sematan dari atas", + "chipActionRename": "Ganti nama", + "chipActionSetCover": "Setel sampul", + "chipActionCreateAlbum": "Membuat album", + + "entryActionCopyToClipboard": "Salinan ke papan", + "entryActionDelete": "Hapus", + "entryActionConvert": "Ubah", + "entryActionExport": "Ekspor", + "entryActionRename": "Ganti nama", + "entryActionRestore": "Pulihkan", + "entryActionRotateCCW": "Putar berlawanan arah jarum jam", + "entryActionRotateCW": "Putar searah jarum jam", + "entryActionFlip": "Balik secara horisontal", + "entryActionPrint": "Cetak", + "entryActionShare": "Bagikan", + "entryActionViewSource": "Lihat sumber", + "entryActionViewMotionPhotoVideo": "Buka Foto bergerak", + "entryActionEdit": "Ubah", + "entryActionOpen": "Buka dengan", + "entryActionSetAs": "Tetapkan sebagai", + "entryActionOpenMap": "Tampilkan di peta", + "entryActionRotateScreen": "Putar layar", + "entryActionAddFavourite": "Tambahkan ke favorit", + "entryActionRemoveFavourite": "Hapus dari favorit", + + "videoActionCaptureFrame": "Tangkap bingkai", + "videoActionPause": "Henti", + "videoActionPlay": "Mainkan", + "videoActionReplay10": "Mundur 10 detik", + "videoActionSkip10": "Majukan 10 detik", + "videoActionSelectStreams": "Pilih trek", + "videoActionSetSpeed": "Kecepatan pemutaran", + "videoActionSettings": "Pengaturan", + + "entryInfoActionEditDate": "Ubah tanggal & waktu", + "entryInfoActionEditLocation": "Ubah lokasi", + "entryInfoActionEditRating": "Ubah peringkat", + "entryInfoActionEditTags": "Ubah tag", + "entryInfoActionRemoveMetadata": "Hapus metadata", + + "filterBinLabel": "Tong sampah", + "filterFavouriteLabel": "Favorit", + "filterLocationEmptyLabel": "Lokasi Tidak ditemukan", + "filterTagEmptyLabel": "Tidak ditag", + "filterRatingUnratedLabel": "Belum diberi peringkat", + "filterRatingRejectedLabel": "Ditolak", + "filterTypeAnimatedLabel": "Teranimasi", + "filterTypeMotionPhotoLabel": "Foto bergerak", + "filterTypePanoramaLabel": "Panorama", + "filterTypeRawLabel": "Raw", + "filterTypeSphericalVideoLabel": "Video 360°", + "filterTypeGeotiffLabel": "GeoTIFF", + "filterMimeImageLabel": "Gambar", + "filterMimeVideoLabel": "Video", + + "coordinateFormatDms": "DMS", + "coordinateFormatDecimal": "Derajat desimal", + "coordinateDms": "{coordinate} {direction}", + "coordinateDmsNorth": "N", + "coordinateDmsSouth": "S", + "coordinateDmsEast": "E", + "coordinateDmsWest": "W", + + "unitSystemMetric": "Metrik", + "unitSystemImperial": "Imperial", + + "videoLoopModeNever": "Tidak pernah", + "videoLoopModeShortOnly": "Hanya video pendek", + "videoLoopModeAlways": "Selalu", + + "mapStyleGoogleNormal": "Google Maps", + "mapStyleGoogleHybrid": "Google Maps (Hybrid)", + "mapStyleGoogleTerrain": "Google Maps (Terrain)", + "mapStyleOsmHot": "Humanitarian OSM", + "mapStyleStamenToner": "Stamen Toner", + "mapStyleStamenWatercolor": "Stamen Watercolor", + + "nameConflictStrategyRename": "Ganti nama", + "nameConflictStrategyReplace": "Ganti", + "nameConflictStrategySkip": "Lewati", + + "keepScreenOnNever": "Tidak pernah", + "keepScreenOnViewerOnly": "Hanya halaman pemirsa", + "keepScreenOnAlways": "Selalu", + + "accessibilityAnimationsRemove": "Mencegah efek layar", + "accessibilityAnimationsKeep": "Simpan efek layar", + + "albumTierNew": "Baru", + "albumTierPinned": "Disemat", + "albumTierSpecial": "Umum", + "albumTierApps": "Aplikasi", + "albumTierRegular": "Lainnya", + + "storageVolumeDescriptionFallbackPrimary": "Penyimpanan internal", + "storageVolumeDescriptionFallbackNonPrimary": "kartu SD", + "rootDirectoryDescription": "direktori root", + "otherDirectoryDescription": "“{name}” direktori", + "storageAccessDialogTitle": "Akses Penyimpanan", + "storageAccessDialogMessage": "Silahkan pilih {directory} dari “{volume}” di layar berikutnya untuk memberikan akses aplikasi ini ke sana.", + "restrictedAccessDialogTitle": "Akses Terbatas", + "restrictedAccessDialogMessage": "Aplikasi ini tidak diizinkan untuk mengubah file di {directory} dari “{volume}”.\n\nSilahkan pakai aplikasi Manager File atau aplikasi gallery untuk gerakkan benda ke direktori lain.", + "notEnoughSpaceDialogTitle": "Tidak Cukup Ruang", + "notEnoughSpaceDialogMessage": "Operasi ini memerlukan {neededSize} ruang kosong di “{volume}” untuk menyelesaikan, tetapi hanya ada {freeSize} tersisa.", + "missingSystemFilePickerDialogTitle": "Pemilih File Sistem Tidak Ada", + "missingSystemFilePickerDialogMessage": "Pemilih file sistem tidak ada atau dinonaktifkan. Harap aktifkan dan coba lagi.", + + "unsupportedTypeDialogTitle": "Jenis Yang Tidak Didukung", + "unsupportedTypeDialogMessage": "{count, plural, other{Operasi ini tidak didukung untuk benda dari jenis berikut: {types}.}}", + + "nameConflictDialogSingleSourceMessage": "Beberapa file di folder tujuan memiliki nama yang sama.", + "nameConflictDialogMultipleSourceMessage": "Beberapa file memiliki nama yang sama.", + + "addShortcutDialogLabel": "Label pintasan", + "addShortcutButtonLabel": "TAMBAH", + + "noMatchingAppDialogTitle": "Tidak Ada Aplikasi Yang Cocok", + "noMatchingAppDialogMessage": "Tidak ada aplikasi yang cocok untuk menangani ini.", + + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Pindah benda ini ke tong sampah?} other{Pindah {count} benda ke tempat sampah?}}", + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus benda ini?} other{Apakah Anda yakin ingin menghapus {count} benda?}}", + + "videoResumeDialogMessage": "Apakah Anda ingin melanjutkan di {time}?", + "videoStartOverButtonLabel": "ULANG DARI AWAL", + "videoResumeButtonLabel": "LANJUT", + + "setCoverDialogTitle": "Setel Sampul", + "setCoverDialogLatest": "Benda terbaru", + "setCoverDialogCustom": "Kustom", + + "hideFilterConfirmationDialogMessage": "Foto dan video yang cocok akan disembunyikan dari koleksi Anda. Anda dapat menampilkannya lagi dari pengaturan “Privasi”.\n\nApakah Anda yakin ingin menyembunyikannya?", + + "newAlbumDialogTitle": "Album Baru", + "newAlbumDialogNameLabel": "Nama album", + "newAlbumDialogNameLabelAlreadyExistsHelper": "Direktori sudah ada", + "newAlbumDialogStorageLabel": "Penyimpanan:", + + "renameAlbumDialogLabel": "Nama baru", + "renameAlbumDialogLabelAlreadyExistsHelper": "Direktori sudah ada", + + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus album ini dan bendanya?} other{Apakah Anda yakin ingin menghapus album ini dan {count} bendanya?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Yakin ingin menghapus album ini dan bendanya?} other{Anda yakin ingin menghapus album ini dan {count} bendanya?}}", + + "exportEntryDialogFormat": "Format:", + "exportEntryDialogWidth": "Lebar", + "exportEntryDialogHeight": "Tinggi", + + "renameEntryDialogLabel": "Nama baru", + + "editEntryDateDialogTitle": "Tanggal & Waktu", + "editEntryDateDialogSetCustom": "Atur tanggal khusus", + "editEntryDateDialogCopyField": "Salin dari tanggal lain", + "editEntryDateDialogExtractFromTitle": "Ekstrak dari judul", + "editEntryDateDialogShift": "Geser", + "editEntryDateDialogSourceFileModifiedDate": "Tanggal modifikasi file", + "editEntryDateDialogTargetFieldsHeader": "Bidang untuk dimodifikasikan", + "editEntryDateDialogHours": "Jam", + "editEntryDateDialogMinutes": "Menit", + + "editEntryLocationDialogTitle": "Lokasi", + "editEntryLocationDialogChooseOnMapTooltip": "Pilih di peta", + "editEntryLocationDialogLatitude": "Garis lintang", + "editEntryLocationDialogLongitude": "Garis bujur", + + "locationPickerUseThisLocationButton": "Gunakan lokasi ini", + + "editEntryRatingDialogTitle": "Peringkat", + + "removeEntryMetadataDialogTitle": "Penghapusan Metadata", + "removeEntryMetadataDialogMore": "Lebih Banyak", + + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP diperlukan untuk memutar video di dalam Foto bergerak.\n\nAnda yakin ingin menghapusnya?", + + "videoSpeedDialogLabel": "Kecepatan pemutaran", + + "videoStreamSelectionDialogVideo": "Video", + "videoStreamSelectionDialogAudio": "Audio", + "videoStreamSelectionDialogText": "Subtitle", + "videoStreamSelectionDialogOff": "Mati", + "videoStreamSelectionDialogTrack": "Trek", + "videoStreamSelectionDialogNoSelection": "Tidak ada Trek yang lain.", + + "genericSuccessFeedback": "Sukses!", + "genericFailureFeedback": "Gagal", + + "menuActionConfigureView": "Lihat", + "menuActionSelect": "Pilih", + "menuActionSelectAll": "Pilih semua", + "menuActionSelectNone": "Pilih tidak ada", + "menuActionMap": "Peta", + "menuActionStats": "Statistik", + + "viewDialogTabSort": "Sortir", + "viewDialogTabGroup": "Grup", + "viewDialogTabLayout": "Tata letak", + + "tileLayoutGrid": "Grid", + "tileLayoutList": "Daftar", + + "aboutPageTitle": "Tentang", + "aboutLinkSources": "Sumber", + "aboutLinkLicense": "Lisensi", + "aboutLinkPolicy": "Aturan Privasi", + + "aboutBug": "Lapor Bug", + "aboutBugSaveLogInstruction": "Simpan log aplikasi ke file", + "aboutBugSaveLogButton": "Simpan", + "aboutBugCopyInfoInstruction": "Salin informasi sistem", + "aboutBugCopyInfoButton": "Salin", + "aboutBugReportInstruction": "Laporkan ke GitHub dengan log dan informasi sistem", + "aboutBugReportButton": "Rapor", + + "aboutCredits": "Kredit", + "aboutCreditsWorldAtlas1": "Aplikasi ini menggunakan file TopoJSON dari", + "aboutCreditsWorldAtlas2": "dibawah Lisensi ISC.", + "aboutCreditsTranslators": "Penerjemah:", + "aboutCreditsTranslatorLine": "{language}: {names}", + + "aboutLicenses": "Lisensi Sumber Terbuka", + "aboutLicensesBanner": "Aplikasi ini menggunakan paket dan pustaka sumber terbuka berikut.", + "aboutLicensesAndroidLibraries": "Perpustakaan Android", + "aboutLicensesFlutterPlugins": "Plugin Flutter", + "aboutLicensesFlutterPackages": "Paket Flutter", + "aboutLicensesDartPackages": "Paket Dart", + "aboutLicensesShowAllButtonLabel": "Tampilkan Semua Lisensi", + + "policyPageTitle": "Aturan Privasi", + + "collectionPageTitle": "Koleksi", + "collectionPickPageTitle": "Pilih", + "collectionSelectPageTitle": "Pilih benda", + + "collectionActionShowTitleSearch": "Tampilkan filter judul", + "collectionActionHideTitleSearch": "Sembunyikan filter judul", + "collectionActionAddShortcut": "Tambahkan pintasan", + "collectionActionEmptyBin": "Kosongkan tong sampah", + "collectionActionCopy": "Salin ke album", + "collectionActionMove": "Pindah ke album", + "collectionActionRescan": "Pindai ulang", + "collectionActionEdit": "Ubah", + + "collectionSearchTitlesHintText": "Cari judul", + + "collectionSortDate": "Lewat tanggal", + "collectionSortSize": "Lewat ukuran", + "collectionSortName": "Lewat nama album & file", + "collectionSortRating": "Lewat peringkat", + + "collectionGroupAlbum": "Lewat album", + "collectionGroupMonth": "Lewat bulan", + "collectionGroupDay": "Lewat hari", + "collectionGroupNone": "Jangan kelompokkan", + + "sectionUnknown": "Tidak dikenal", + "dateToday": "Hari ini", + "dateYesterday": "Kemaren", + "dateThisMonth": "Bulan ini", + "collectionDeleteFailureFeedback": "{count, plural, other{Gagal untuk menghapus {count} benda}}", + "collectionCopyFailureFeedback": "{count, plural, other{Gagal untuk menyalin {count} benda}}", + "collectionMoveFailureFeedback": "{count, plural, other{Gagal untuk menggerakkan {count} benda}}", + "collectionEditFailureFeedback": "{count, plural, other{Gagal untuk mengubah {count} benda}}", + "collectionExportFailureFeedback": "{count, plural, other{Gagal untuk mengekspor {count} halaman}}", + "collectionCopySuccessFeedback": "{count, plural, other{Menyalin {count} benda}}", + "collectionMoveSuccessFeedback": "{count, plural, other{Menggerakkan {count} benda}}", + "collectionEditSuccessFeedback": "{count, plural, other{Mengubah {count} benda}}", + + "collectionEmptyFavourites": "Tidak ada favorit", + "collectionEmptyVideos": "Tidak ada video", + "collectionEmptyImages": "Tidak ada gambar", + + "collectionSelectSectionTooltip": "Pilih bagian", + "collectionDeselectSectionTooltip": "Batalkan pilihan bagian", + + "drawerCollectionAll": "Semua koleksi", + "drawerCollectionFavourites": "Favorit", + "drawerCollectionImages": "Gambar", + "drawerCollectionVideos": "Video", + "drawerCollectionAnimated": "Teranimasi", + "drawerCollectionMotionPhotos": "Foto bergerak", + "drawerCollectionPanoramas": "Panorama", + "drawerCollectionRaws": "Foto Raw", + "drawerCollectionSphericalVideos": "Video 360°", + + "chipSortDate": "Lewat tanggal", + "chipSortName": "Lewat nama", + "chipSortCount": "Lewat jumlah benda", + + "albumGroupTier": "Lewat tingkat", + "albumGroupVolume": "Lewat volume penyimpanan", + "albumGroupNone": "Jangan kelompokkan", + + "albumPickPageTitleCopy": "Salin ke Album", + "albumPickPageTitleExport": "Ekspor ke Album", + "albumPickPageTitleMove": "Pindah ke Album", + "albumPickPageTitlePick": "Pilih Album", + + "albumCamera": "Kamera", + "albumDownload": "Download", + "albumScreenshots": "Tangkapan layar", + "albumScreenRecordings": "Rekaman layar", + "albumVideoCaptures": "Tangkapan Video", + + "albumPageTitle": "Album", + "albumEmpty": "Tidak ada album", + "createAlbumTooltip": "Buat album", + "createAlbumButtonLabel": "BUAT", + "newFilterBanner": "baru", + + "countryPageTitle": "Negara", + "countryEmpty": "Tidak ada negara", + + "tagPageTitle": "Tag", + "tagEmpty": "Tidak ada tag", + + "binPageTitle": "Tong Sampah", + + "searchCollectionFieldHint": "Cari koleksi", + "searchSectionRecent": "Terkini", + "searchSectionAlbums": "Album", + "searchSectionCountries": "Negara", + "searchSectionPlaces": "Tempat", + "searchSectionTags": "Tag", + "searchSectionRating": "Peringkat", + + "settingsPageTitle": "Pengaturan", + "settingsSystemDefault": "Sistem", + "settingsDefault": "Default", + + "settingsActionExport": "Ekspor", + "settingsActionImport": "Impor", + + "appExportCovers": "Sampul", + "appExportFavourites": "Favorit", + "appExportSettings": "Pengaturan", + + "settingsSectionNavigation": "Navigasi", + "settingsHome": "Beranda", + "settingsKeepScreenOnTile": "Biarkan layarnya menyala", + "settingsKeepScreenOnTitle": "Biarkan Layarnya Menyala", + "settingsDoubleBackExit": "Ketuk “kembali” dua kali untuk keluar", + + "settingsConfirmationDialogTile": "Dialog konfirmasi", + "settingsConfirmationDialogTitle": "Dialog Konfirmasi", + "settingsConfirmationDialogDeleteItems": "Tanya sebelum menghapus benda selamanya", + "settingsConfirmationDialogMoveToBinItems": "Tanya sebelum memindahkan benda ke tempat sampah", + "settingsNavigationDrawerTile": "Menu navigasi", + "settingsNavigationDrawerEditorTitle": "Menu Navigasi", + "settingsNavigationDrawerBanner": "Sentuh dan tahan untuk memindahkan dan menyusun ulang benda menu.", + "settingsNavigationDrawerTabTypes": "Tipe", + "settingsNavigationDrawerTabAlbums": "Album", + "settingsNavigationDrawerTabPages": "Halaman", + "settingsNavigationDrawerAddAlbum": "Tambahkan album", + + "settingsSectionThumbnails": "Thumbnail", + "settingsThumbnailShowFavouriteIcon": "Tampilkan ikon favorit", + "settingsThumbnailShowLocationIcon": "Tampilkan ikon lokasi", + "settingsThumbnailShowMotionPhotoIcon": "Tampilkan ikon Foto bergerak", + "settingsThumbnailShowRating": "Tampilkan peringkat", + "settingsThumbnailShowRawIcon": "Tampilkan ikon raw", + "settingsThumbnailShowVideoDuration": "Tampilkan durasi video", + + "settingsCollectionQuickActionsTile": "Aksi cepat", + "settingsCollectionQuickActionEditorTitle": "Aksi Cepat", + "settingsCollectionQuickActionTabBrowsing": "Menjelajah", + "settingsCollectionQuickActionTabSelecting": "Memilih", + "settingsCollectionBrowsingQuickActionEditorBanner": "Sentuh dan tahan untuk memindahkan tombol dan memilih tindakan yang ditampilkan saat menelusuri benda.", + "settingsCollectionSelectionQuickActionEditorBanner": "Sentuh dan tahan untuk memindahkan tombol dan memilih tindakan yang ditampilkan saat memilih benda.", + + "settingsSectionViewer": "Penonton", + "settingsViewerUseCutout": "Gunakan area potongan", + "settingsViewerMaximumBrightness": "Kecerahan maksimum", + "settingsMotionPhotoAutoPlay": "Putar foto bergerak otomatis", + "settingsImageBackground": "Latar belakang gambar", + + "settingsViewerQuickActionsTile": "Aksi cepat", + "settingsViewerQuickActionEditorTitle": "Aksi Cepat", + "settingsViewerQuickActionEditorBanner": "Sentuh dan tahan untuk memindahkan tombol dan memilih tindakan yang ditampilkan di penampil.", + "settingsViewerQuickActionEditorDisplayedButtons": "Tombol yang Ditampilkan", + "settingsViewerQuickActionEditorAvailableButtons": "Tombol yang tersedia", + "settingsViewerQuickActionEmpty": "Tidak ada tombol", + + "settingsViewerOverlayTile": "Hamparan", + "settingsViewerOverlayTitle": "Hamparan", + "settingsViewerShowOverlayOnOpening": "Tampilkan saat pembukaan", + "settingsViewerShowMinimap": "Tampilkan minimap", + "settingsViewerShowInformation": "Tampilkan informasi", + "settingsViewerShowInformationSubtitle": "Tampilkan judul, tanggal, lokasi, dll.", + "settingsViewerShowShootingDetails": "Tampilkan detail pemotretan", + "settingsViewerEnableOverlayBlurEffect": "Efek Kabur", + + "settingsVideoPageTitle": "Pengaturan Video", + "settingsSectionVideo": "Video", + "settingsVideoShowVideos": "Tampilkan video", + "settingsVideoEnableHardwareAcceleration": "Akselerasi perangkat keras", + "settingsVideoEnableAutoPlay": "Putar otomatis", + "settingsVideoLoopModeTile": "Putar ulang", + "settingsVideoLoopModeTitle": "Putar Ulang", + "settingsVideoQuickActionsTile": "Aksi cepat untuk video", + "settingsVideoQuickActionEditorTitle": "Aksi Cepat", + + "settingsSubtitleThemeTile": "Subtitle", + "settingsSubtitleThemeTitle": "Subtitle", + "settingsSubtitleThemeSample": "Ini adalah sampel.", + "settingsSubtitleThemeTextAlignmentTile": "Perataan teks", + "settingsSubtitleThemeTextAlignmentTitle": "Perataan Teks", + "settingsSubtitleThemeTextSize": "Ukuran teks", + "settingsSubtitleThemeShowOutline": "Tampilkan garis besar dan bayangan", + "settingsSubtitleThemeTextColor": "Warna teks", + "settingsSubtitleThemeTextOpacity": "Opasitas teks", + "settingsSubtitleThemeBackgroundColor": "Warna latar belakang", + "settingsSubtitleThemeBackgroundOpacity": "Opasitas latar belakang", + "settingsSubtitleThemeTextAlignmentLeft": "Kiri", + "settingsSubtitleThemeTextAlignmentCenter": "Tengah", + "settingsSubtitleThemeTextAlignmentRight": "Kanan", + + "settingsSectionPrivacy": "Privasi", + "settingsAllowInstalledAppAccess": "Izinkan akses ke inventori aplikasi", + "settingsAllowInstalledAppAccessSubtitle": "Digunakan untuk meningkatkan tampilan album", + "settingsAllowErrorReporting": "Izinkan pelaporan kesalahan anonim", + "settingsSaveSearchHistory": "Simpan riwayat pencarian", + "settingsEnableBin": "Gunakan tong sampah", + "settingsEnableBinSubtitle": "Simpan benda yang dihapus selama 30 hari", + + "settingsHiddenItemsTile": "Benda tersembunyi", + "settingsHiddenItemsTitle": "Benda Tersembunyi", + + "settingsHiddenFiltersTitle": "Filter Tersembunyi", + "settingsHiddenFiltersBanner": "Foto dan video filter tersembunyi yang cocok tidak akan muncul di koleksi Anda.", + "settingsHiddenFiltersEmpty": "Tidak ada filter tersembunyi", + + "settingsHiddenPathsTitle": "Jalan Tersembunyi", + "settingsHiddenPathsBanner": "Foto dan video di folder ini, atau subfoldernya, tidak akan muncul di koleksi Anda.", + "addPathTooltip": "Tambahkan jalan", + + "settingsStorageAccessTile": "Akses penyimpanan", + "settingsStorageAccessTitle": "Akses Penyimpanan", + "settingsStorageAccessBanner": "Beberapa direktori memerlukan pemberian akses eksplisit untuk memodifikasi file di dalamnya. Anda dapat meninjau di sini direktori yang sebelumnya Anda beri akses.", + "settingsStorageAccessEmpty": "Tidak ada akses", + "settingsStorageAccessRevokeTooltip": "Tarik kembali", + + "settingsSectionAccessibility": "Aksesibilitas", + "settingsRemoveAnimationsTile": "Hapus animasi", + "settingsRemoveAnimationsTitle": "Hapus Animasi", + "settingsTimeToTakeActionTile": "Saatnya untuk mengambil tindakan", + "settingsTimeToTakeActionTitle": "Saatnya Bertindak", + + "settingsSectionLanguage": "Bahasa & Format", + "settingsLanguage": "Bahasa", + "settingsCoordinateFormatTile": "Format koordinat", + "settingsCoordinateFormatTitle": "Format Koordinat", + "settingsUnitSystemTile": "Unit", + "settingsUnitSystemTitle": "Unit", + + "statsPageTitle": "Statistik", + "statsWithGps": "{count, plural, other{{count} benda dengan lokasi}}", + "statsTopCountries": "Negara Teratas", + "statsTopPlaces": "Tempat Teratas", + "statsTopTags": "Tag Teratas", + + "viewerOpenPanoramaButtonLabel": "BUKA PANORAMA", + "viewerErrorUnknown": "Ups!", + "viewerErrorDoesNotExist": "File tidak ada lagi.", + + "viewerInfoPageTitle": "Info", + "viewerInfoBackToViewerTooltip": "Kembali ke pemirsa", + + "viewerInfoUnknown": "tidak dikenal", + "viewerInfoLabelTitle": "Judul", + "viewerInfoLabelDate": "Tanggal", + "viewerInfoLabelResolution": "Resolusi", + "viewerInfoLabelSize": "Ukuran", + "viewerInfoLabelUri": "URI", + "viewerInfoLabelPath": "Jalan", + "viewerInfoLabelDuration": "Durasi", + "viewerInfoLabelOwner": "Dimiliki oleh", + "viewerInfoLabelCoordinates": "Koordinat", + "viewerInfoLabelAddress": "Alamat", + + "mapStyleTitle": "Gaya Peta", + "mapStyleTooltip": "Pilih gaya peta", + "mapZoomInTooltip": "Perbesar", + "mapZoomOutTooltip": "Perkecil", + "mapPointNorthUpTooltip": "Arahkan ke utara ke atas", + "mapAttributionOsmHot": "Data peta © [OpenStreetMap](https://www.openstreetmap.org/copyright) kontributor • Tile oleh [HOT](https://www.hotosm.org/) • Diselenggarakan oleh [OSM France](https://openstreetmap.fr/)", + "mapAttributionStamen": "Data peta © [OpenStreetMap](https://www.openstreetmap.org/copyright) kontributor • Tile oleh [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)", + "openMapPageTooltip": "Lihat di halaman Peta", + "mapEmptyRegion": "Tidak ada gambar di wilayah ini", + + "viewerInfoOpenEmbeddedFailureFeedback": "Gagal mengekstrak data yang disematkan", + "viewerInfoOpenLinkText": "Buka", + "viewerInfoViewXmlLinkText": "Tampilkan XML", + + "viewerInfoSearchFieldLabel": "Cari metadata", + "viewerInfoSearchEmpty": "Tidak ada kata kunci yang cocok", + "viewerInfoSearchSuggestionDate": "Tanggal & waktu", + "viewerInfoSearchSuggestionDescription": "Deskripsi", + "viewerInfoSearchSuggestionDimensions": "Dimensi", + "viewerInfoSearchSuggestionResolution": "Resolusi", + "viewerInfoSearchSuggestionRights": "Hak", + + "tagEditorPageTitle": "Ubah Tag", + "tagEditorPageNewTagFieldLabel": "Tag baru", + "tagEditorPageAddTagTooltip": "Tambah tag", + "tagEditorSectionRecent": "Terkini", + + "panoramaEnableSensorControl": "Aktifkan kontrol sensor", + "panoramaDisableSensorControl": "Nonaktifkan kontrol sensor", + + "sourceViewerPageTitle": "Sumber", + + "filePickerShowHiddenFiles": "Tampilkan file tersembunyi", + "filePickerDoNotShowHiddenFiles": "Jangan tampilkan file tersembunyi", + "filePickerOpenFrom": "Buka dari", + "filePickerNoItems": "Tidak ada benda", + "filePickerUseThisFolder": "Gunakan folder ini" +} diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 2edf0589b..abe37bbcc 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, other{{seconds}초}}", "timeMinutes": "{minutes, plural, other{{minutes}분}}", + "timeDays": "{days, plural, other{{days}일}}", "focalLength": "{length} mm", "applyButtonLabel": "확인", @@ -27,6 +28,7 @@ "resetButtonTooltip": "복원", "doubleBackExitMessage": "종료하려면 한번 더 누르세요.", + "doNotAskAgain": "다시 묻지 않기", "sourceStateLoading": "로딩 중", "sourceStateCataloguing": "분석 중", @@ -46,9 +48,10 @@ "entryActionCopyToClipboard": "클립보드에 복사", "entryActionDelete": "삭제", + "entryActionConvert": "변환", "entryActionExport": "내보내기", - "entryActionInfo": "상세정보", "entryActionRename": "이름 변경", + "entryActionRestore": "복원", "entryActionRotateCCW": "좌회전", "entryActionRotateCW": "우회전", "entryActionFlip": "좌우 뒤집기", @@ -56,10 +59,10 @@ "entryActionShare": "공유", "entryActionViewSource": "소스 코드 보기", "entryActionViewMotionPhotoVideo": "모션 포토 보기", - "entryActionEdit": "편집…", - "entryActionOpen": "다른 앱에서 열기…", - "entryActionSetAs": "다음 용도로 사용…", - "entryActionOpenMap": "지도 앱에서 보기…", + "entryActionEdit": "편집", + "entryActionOpen": "다른 앱에서 열기", + "entryActionSetAs": "다음 용도로 사용", + "entryActionOpenMap": "지도 앱에서 보기", "entryActionRotateScreen": "화면 회전", "entryActionAddFavourite": "즐겨찾기에 추가", "entryActionRemoveFavourite": "즐겨찾기에서 삭제", @@ -79,6 +82,7 @@ "entryInfoActionEditTags": "태그 수정", "entryInfoActionRemoveMetadata": "메타데이터 삭제", + "filterBinLabel": "휴지통", "filterFavouriteLabel": "즐겨찾기", "filterLocationEmptyLabel": "장소 없음", "filterTagEmptyLabel": "태그 없음", @@ -157,6 +161,7 @@ "noMatchingAppDialogTitle": "처리할 앱 없음", "noMatchingAppDialogMessage": "이 작업을 처리할 수 있는 앱이 없습니다.", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 휴지통으로 이동하시겠습니까?} other{항목 {count}개를 휴지통으로 이동하시겠습니까?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 삭제하시겠습니까?} other{항목 {count}개를 삭제하시겠습니까?}}", "videoResumeDialogMessage": "{time}부터 재개하시겠습니까?", @@ -267,11 +272,12 @@ "collectionPageTitle": "미디어", "collectionPickPageTitle": "항목 선택", - "collectionSelectionPageTitle": "{count, plural, =0{항목 선택} other{{count}개}}", + "collectionSelectPageTitle": "항목 선택", "collectionActionShowTitleSearch": "제목 필터 보기", "collectionActionHideTitleSearch": "제목 필터 숨기기", "collectionActionAddShortcut": "홈 화면에 추가", + "collectionActionEmptyBin": "휴지통 비우기", "collectionActionCopy": "앨범으로 복사", "collectionActionMove": "앨범으로 이동", "collectionActionRescan": "새로 분석", @@ -350,6 +356,8 @@ "tagPageTitle": "태그", "tagEmpty": "태그가 없습니다", + "binPageTitle": "휴지통", + "searchCollectionFieldHint": "미디어 검색", "searchSectionRecent": "최근 검색기록", "searchSectionAlbums": "앨범", @@ -375,6 +383,11 @@ "settingsKeepScreenOnTitle": "화면 자동 꺼짐 방지", "settingsDoubleBackExit": "뒤로가기 두번 눌러 앱 종료하기", + "settingsConfirmationDialogTile": "확정 대화상자", + "settingsConfirmationDialogTitle": "확정 대화상자", + "settingsConfirmationDialogDeleteItems": "항목을 완전히 삭제 시", + "settingsConfirmationDialogMoveToBinItems": "항목을 휴지통으로 이동 시", + "settingsNavigationDrawerTile": "탐색 메뉴", "settingsNavigationDrawerEditorTitle": "탐색 메뉴", "settingsNavigationDrawerBanner": "항목을 길게 누른 후 이동하여 탐색 메뉴에 표시될 항목의 순서를 수정하세요.", @@ -450,6 +463,8 @@ "settingsAllowInstalledAppAccessSubtitle": "앨범 표시 개선을 위해", "settingsAllowErrorReporting": "오류 보고서 보내기", "settingsSaveSearchHistory": "검색기록", + "settingsEnableBin": "휴지통 사용", + "settingsEnableBinSubtitle": "삭제한 항목을 30일 동안 보관하기", "settingsHiddenItemsTile": "숨겨진 항목", "settingsHiddenItemsTitle": "숨겨진 항목", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index b4dfef910..948e20c12 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}", "timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}", + "timeDays": "{days, plural, =1{1 dia} other{{days} dias}}", "focalLength": "{length} mm", "applyButtonLabel": "APLIQUE", @@ -27,6 +28,7 @@ "resetButtonTooltip": "Resetar", "doubleBackExitMessage": "Toque em “voltar” novamente para sair.", + "doNotAskAgain": "Não pergunte novamente", "sourceStateLoading": "Carregando", "sourceStateCataloguing": "Catalogação", @@ -47,8 +49,9 @@ "entryActionCopyToClipboard": "Copiar para área de transferência", "entryActionDelete": "Excluir", "entryActionExport": "Exportar", - "entryActionInfo": "Informações", + "entryActionConvert": "Converter", "entryActionRename": "Renomear", + "entryActionRestore": "Restaurar", "entryActionRotateCCW": "Rotacionar para esquerda", "entryActionRotateCW": "Rotacionar para direita", "entryActionFlip": "Virar horizontalmente", @@ -56,10 +59,10 @@ "entryActionShare": "Compartilhado", "entryActionViewSource": "Ver fonte", "entryActionViewMotionPhotoVideo": "Abrir foto em movimento", - "entryActionEdit": "Editar com…", - "entryActionOpen": "Abrir com…", - "entryActionSetAs": "Definir como…", - "entryActionOpenMap": "Mostrar no aplicativo de mapa…", + "entryActionEdit": "Editar", + "entryActionOpen": "Abrir com", + "entryActionSetAs": "Definir como", + "entryActionOpenMap": "Mostrar no aplicativo de mapa", "entryActionRotateScreen": "Girar a tela", "entryActionAddFavourite": "Adicionar aos favoritos", "entryActionRemoveFavourite": "Remova dos favoritos", @@ -79,6 +82,7 @@ "entryInfoActionEditTags": "Editar etiquetas", "entryInfoActionRemoveMetadata": "Remover metadados", + "filterBinLabel": "Lixeira", "filterFavouriteLabel": "Favorito", "filterLocationEmptyLabel": "Não localizado", "filterTagEmptyLabel": "Sem etiqueta", @@ -157,6 +161,7 @@ "noMatchingAppDialogTitle": "Nenhum aplicativo correspondente", "noMatchingAppDialogMessage": "Não há aplicativos que possam lidar com isso.", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Mover esse item para a lixeira?} other{Mova estes {count} itens para a lixeira?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Tem certeza de que deseja excluir este item?} other{Tem certeza de que deseja excluir estes {count} itens?}}", "videoResumeDialogMessage": "Deseja continuar jogando em {time}?", @@ -267,11 +272,12 @@ "collectionPageTitle": "Coleção", "collectionPickPageTitle": "Escolher", - "collectionSelectionPageTitle": "{count, plural, =0{Selecionar itens} =1{1 item} other{{count} itens}}", + "collectionSelectPageTitle": "Selecionar itens", "collectionActionShowTitleSearch": "Mostrar filtro de título", "collectionActionHideTitleSearch": "Ocultar filtro de título", "collectionActionAddShortcut": "Adicionar atalho", + "collectionActionEmptyBin": "Caixa vazia", "collectionActionCopy": "Copiar para o álbum", "collectionActionMove": "Mover para o álbum", "collectionActionRescan": "Reexaminar", @@ -350,6 +356,8 @@ "tagPageTitle": "Etiquetas", "tagEmpty": "Sem etiquetas", + "binPageTitle": "Lixeira", + "searchCollectionFieldHint": "Pesquisar coleção", "searchSectionRecent": "Recente", "searchSectionAlbums": "Álbuns", @@ -375,6 +383,11 @@ "settingsKeepScreenOnTitle": "Manter a tela ligada", "settingsDoubleBackExit": "Toque em “voltar” duas vezes para sair", + "settingsConfirmationDialogTile": "Caixas de diálogo de confirmação", + "settingsConfirmationDialogTitle": "Caixas de diálogo de confirmação", + "settingsConfirmationDialogDeleteItems": "Pergunte antes de excluir itens para sempre", + "settingsConfirmationDialogMoveToBinItems": "Pergunte antes de mover itens para a lixeira", + "settingsNavigationDrawerTile": "Menu de navegação", "settingsNavigationDrawerEditorTitle": "Menu de navegação", "settingsNavigationDrawerBanner": "Toque e segure para mover e reordenar os itens do menu.", @@ -450,6 +463,8 @@ "settingsAllowInstalledAppAccessSubtitle": "Usado para melhorar a exibição do álbum", "settingsAllowErrorReporting": "Permitir relatórios de erros anônimos", "settingsSaveSearchHistory": "Salvar histórico de pesquisa", + "settingsEnableBin": "Usar lixeira", + "settingsEnableBinSubtitle": "Manter itens excluídos por 30 dias", "settingsHiddenItemsTile": "Itens ocultos", "settingsHiddenItemsTitle": "Itens ocultos", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 06d891c34..86b70ebe5 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -7,6 +7,7 @@ "timeSeconds": "{seconds, plural, =1{1 секунда} few{{seconds} секунды} other{{seconds} секунд}}", "timeMinutes": "{minutes, plural, =1{1 минута} few{{minutes} минуты} other{{minutes} минут}}", + "timeDays": "{days, plural, =1{1 день} few{{days} дня} other{{days} дней}}", "focalLength": "{length} mm", "applyButtonLabel": "ПРИМЕНИТЬ", @@ -27,6 +28,7 @@ "resetButtonTooltip": "Сбросить", "doubleBackExitMessage": "Нажмите «назад» еще раз, чтобы выйти.", + "doNotAskAgain": "Больше не спрашивать", "sourceStateLoading": "Загрузка", "sourceStateCataloguing": "Каталогизация", @@ -47,8 +49,8 @@ "entryActionCopyToClipboard": "Скопировать в буфер обмена", "entryActionDelete": "Удалить", "entryActionExport": "Экспорт", - "entryActionInfo": "Информация", "entryActionRename": "Переименовать", + "entryActionRestore": "Восстановить", "entryActionRotateCCW": "Повернуть против часовой стрелки", "entryActionRotateCW": "Повернуть по часовой стрелки", "entryActionFlip": "Отразить по горизонтали", @@ -56,10 +58,10 @@ "entryActionShare": "Поделиться", "entryActionViewSource": "Посмотреть источник", "entryActionViewMotionPhotoVideo": "Открыть «Живые фото»", - "entryActionEdit": "Изменить с помощью…", - "entryActionOpen": "Открыть с помощью…", - "entryActionSetAs": "Установить как…", - "entryActionOpenMap": "Показать на карте…", + "entryActionEdit": "Изменить", + "entryActionOpen": "Открыть с помощью", + "entryActionSetAs": "Установить как", + "entryActionOpenMap": "Показать на карте", "entryActionRotateScreen": "Повернуть экран", "entryActionAddFavourite": "Добавить в избранное", "entryActionRemoveFavourite": "Удалить из избранного", @@ -79,6 +81,7 @@ "entryInfoActionEditTags": "Изменить теги", "entryInfoActionRemoveMetadata": "Удалить метаданные", + "filterBinLabel": "Корзина", "filterFavouriteLabel": "Избранное", "filterLocationEmptyLabel": "Без местоположения", "filterTagEmptyLabel": "Без тегов", @@ -157,6 +160,7 @@ "noMatchingAppDialogTitle": "Нет подходящего приложения", "noMatchingAppDialogMessage": "Нет приложений, которые могли бы с этим справиться.", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Переместить этот элемент в корзину?} few{Переместить эти {count} элемента в корзину?} other{Переместить эти {count} элементов в корзину?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Вы уверены, что хотите удалить этот объект?} few{Вы уверены, что хотите удалить эти {count} объекта?} other{Вы уверены, что хотите удалить эти {count} объектов?}}", "videoResumeDialogMessage": "Хотите ли вы возобновить воспроизведение на {time}?", @@ -267,11 +271,12 @@ "collectionPageTitle": "Коллекция", "collectionPickPageTitle": "Выбрать", - "collectionSelectionPageTitle": "{count, plural, =0{Выберите объекты} =1{1 объект} few{{count} объекта} other{{count} объектов}}", + "collectionSelectPageTitle": "Выберите объекты", "collectionActionShowTitleSearch": "Показать фильтр заголовка", "collectionActionHideTitleSearch": "Скрыть фильтр заголовка", "collectionActionAddShortcut": "Добавить ярлык", + "collectionActionEmptyBin": "Очистить корзину", "collectionActionCopy": "Скопировать в альбом", "collectionActionMove": "Переместить в альбом", "collectionActionRescan": "Пересканировать", @@ -350,6 +355,8 @@ "tagPageTitle": "Теги", "tagEmpty": "Нет тегов", + "binPageTitle": "Корзина", + "searchCollectionFieldHint": "Поиск по коллекции", "searchSectionRecent": "Недавние", "searchSectionAlbums": "Альбомы", @@ -375,6 +382,11 @@ "settingsKeepScreenOnTitle": "Держать экран включенным", "settingsDoubleBackExit": "Дважды нажмите «Назад», чтобы выйти", + "settingsConfirmationDialogTile": "Диалоги подтверждения", + "settingsConfirmationDialogTitle": "Диалоги подтверждения", + "settingsConfirmationDialogDeleteItems": "Спросить, прежде чем удалять элементы навсегда", + "settingsConfirmationDialogMoveToBinItems": "Спросить, прежде чем перемещать элементы в корзину", + "settingsNavigationDrawerTile": "Навигационное меню", "settingsNavigationDrawerEditorTitle": "Навигационное меню", "settingsNavigationDrawerBanner": "Нажмите и удерживайте, чтобы переместить и изменить порядок пунктов меню.", @@ -450,6 +462,8 @@ "settingsAllowInstalledAppAccessSubtitle": "Используется для улучшения отображения альбомов", "settingsAllowErrorReporting": "Разрешить анонимную отправку логов", "settingsSaveSearchHistory": "Сохранять историю поиска", + "settingsEnableBin": "Использовать корзину", + "settingsEnableBinSubtitle": "Хранить удалённые элементы в течение 30 дней", "settingsHiddenItemsTile": "Скрытые объекты", "settingsHiddenItemsTitle": "Скрытые объекты", diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 4a8c98248..2528b62d7 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -1,5 +1,5 @@ +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; @@ -7,10 +7,12 @@ enum EntryAction { addShortcut, copyToClipboard, delete, - export, - info, + restore, + convert, print, rename, + copy, + move, share, toggleFavourite, // raster @@ -31,25 +33,32 @@ enum EntryAction { } class EntryActions { - static const inApp = [ - EntryAction.info, - EntryAction.toggleFavourite, + static const topLevel = [ EntryAction.share, - EntryAction.delete, + EntryAction.edit, EntryAction.rename, - EntryAction.export, - EntryAction.addShortcut, - EntryAction.copyToClipboard, - EntryAction.print, + EntryAction.delete, + EntryAction.copy, + EntryAction.move, + EntryAction.toggleFavourite, EntryAction.viewSource, EntryAction.rotateScreen, ]; - static const externalApp = [ - EntryAction.edit, + static const export = [ + EntryAction.convert, + EntryAction.addShortcut, + EntryAction.copyToClipboard, + EntryAction.print, EntryAction.open, - EntryAction.setAs, EntryAction.openMap, + EntryAction.setAs, + ]; + + static const exportExternal = [ + EntryAction.open, + EntryAction.openMap, + EntryAction.setAs, ]; static const pageActions = [ @@ -57,6 +66,12 @@ class EntryActions { EntryAction.rotateCW, EntryAction.flip, ]; + + static const trashed = [ + EntryAction.delete, + EntryAction.restore, + EntryAction.debug, + ]; } extension ExtraEntryAction on EntryAction { @@ -68,14 +83,18 @@ extension ExtraEntryAction on EntryAction { return context.l10n.entryActionCopyToClipboard; case EntryAction.delete: return context.l10n.entryActionDelete; - case EntryAction.export: - return context.l10n.entryActionExport; - case EntryAction.info: - return context.l10n.entryActionInfo; + case EntryAction.restore: + return context.l10n.entryActionRestore; + case EntryAction.convert: + return context.l10n.entryActionConvert; case EntryAction.print: return context.l10n.entryActionPrint; case EntryAction.rename: return context.l10n.entryActionRename; + case EntryAction.copy: + return context.l10n.collectionActionCopy; + case EntryAction.move: + return context.l10n.collectionActionMove; case EntryAction.share: return context.l10n.entryActionShare; case EntryAction.toggleFavourite: @@ -109,15 +128,12 @@ extension ExtraEntryAction on EntryAction { } } - Widget? getIcon() { - final icon = getIconData(); - if (icon == null) return null; - - final child = Icon(icon); + Widget getIcon() { + final child = Icon(getIconData()); switch (this) { case EntryAction.debug: return ShaderMask( - shaderCallback: Themes.debugGradient.createShader, + shaderCallback: AColors.debugGradient.createShader, child: child, ); default: @@ -125,7 +141,7 @@ extension ExtraEntryAction on EntryAction { } } - IconData? getIconData() { + IconData getIconData() { switch (this) { case EntryAction.addShortcut: return AIcons.addShortcut; @@ -133,14 +149,18 @@ extension ExtraEntryAction on EntryAction { return AIcons.clipboard; case EntryAction.delete: return AIcons.delete; - case EntryAction.export: - return AIcons.saveAs; - case EntryAction.info: - return AIcons.info; + case EntryAction.restore: + return AIcons.restore; + case EntryAction.convert: + return AIcons.convert; case EntryAction.print: return AIcons.print; case EntryAction.rename: return AIcons.rename; + case EntryAction.copy: + return AIcons.copy; + case EntryAction.move: + return AIcons.move; case EntryAction.share: return AIcons.share; case EntryAction.toggleFavourite: @@ -158,10 +178,13 @@ extension ExtraEntryAction on EntryAction { return AIcons.vector; // external case EntryAction.edit: + return AIcons.edit; case EntryAction.open: + return AIcons.openOutside; case EntryAction.openMap: + return AIcons.map; case EntryAction.setAs: - return null; + return AIcons.setAs; // platform case EntryAction.rotateScreen: return AIcons.rotateScreen; diff --git a/lib/model/actions/entry_info_actions.dart b/lib/model/actions/entry_info_actions.dart index d23c65e02..1498c8066 100644 --- a/lib/model/actions/entry_info_actions.dart +++ b/lib/model/actions/entry_info_actions.dart @@ -1,3 +1,4 @@ +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; @@ -11,6 +12,8 @@ enum EntryInfoAction { removeMetadata, // motion photo viewMotionPhotoVideo, + // debug + debug, } class EntryInfoActions { @@ -41,11 +44,23 @@ extension ExtraEntryInfoAction on EntryInfoAction { // motion photo case EntryInfoAction.viewMotionPhotoVideo: return context.l10n.entryActionViewMotionPhotoVideo; + // debug + case EntryInfoAction.debug: + return 'Debug'; } } Widget getIcon() { - return Icon(_getIconData()); + final child = Icon(_getIconData()); + switch (this) { + case EntryInfoAction.debug: + return ShaderMask( + shaderCallback: AColors.debugGradient.createShader, + child: child, + ); + default: + return child; + } } IconData _getIconData() { @@ -64,6 +79,9 @@ extension ExtraEntryInfoAction on EntryInfoAction { // motion photo case EntryInfoAction.viewMotionPhotoVideo: return AIcons.motionPhoto; + // debug + case EntryInfoAction.debug: + return AIcons.debug; } } } diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart index 5ef1ac427..8df63b991 100644 --- a/lib/model/actions/entry_set_actions.dart +++ b/lib/model/actions/entry_set_actions.dart @@ -12,6 +12,7 @@ enum EntrySetAction { searchCollection, toggleTitleSearch, addShortcut, + emptyBin, // browsing or selecting map, stats, @@ -19,6 +20,7 @@ enum EntrySetAction { // selecting share, delete, + restore, copy, move, toggleFavourite, @@ -40,7 +42,18 @@ class EntrySetActions { EntrySetAction.selectNone, ]; - static const browsing = [ + static const pageBrowsing = [ + EntrySetAction.searchCollection, + EntrySetAction.toggleTitleSearch, + EntrySetAction.addShortcut, + EntrySetAction.map, + EntrySetAction.stats, + EntrySetAction.rescan, + EntrySetAction.emptyBin, + ]; + + // exclude bin related actions + static const collectionEditorBrowsing = [ EntrySetAction.searchCollection, EntrySetAction.toggleTitleSearch, EntrySetAction.addShortcut, @@ -49,7 +62,21 @@ class EntrySetActions { EntrySetAction.rescan, ]; - static const selection = [ + static const pageSelection = [ + EntrySetAction.share, + EntrySetAction.delete, + EntrySetAction.restore, + EntrySetAction.copy, + EntrySetAction.move, + EntrySetAction.toggleFavourite, + EntrySetAction.map, + EntrySetAction.stats, + EntrySetAction.rescan, + // editing actions are in their subsection + ]; + + // exclude bin related actions + static const collectionEditorSelection = [ EntrySetAction.share, EntrySetAction.delete, EntrySetAction.copy, @@ -60,6 +87,14 @@ class EntrySetActions { EntrySetAction.rescan, // editing actions are in their subsection ]; + + static const edit = [ + EntrySetAction.editDate, + EntrySetAction.editLocation, + EntrySetAction.editRating, + EntrySetAction.editTags, + EntrySetAction.removeMetadata, + ]; } extension ExtraEntrySetAction on EntrySetAction { @@ -82,6 +117,8 @@ extension ExtraEntrySetAction on EntrySetAction { return context.l10n.collectionActionShowTitleSearch; case EntrySetAction.addShortcut: return context.l10n.collectionActionAddShortcut; + case EntrySetAction.emptyBin: + return context.l10n.collectionActionEmptyBin; // browsing or selecting case EntrySetAction.map: return context.l10n.menuActionMap; @@ -94,6 +131,8 @@ extension ExtraEntrySetAction on EntrySetAction { return context.l10n.entryActionShare; case EntrySetAction.delete: return context.l10n.entryActionDelete; + case EntrySetAction.restore: + return context.l10n.entryActionRestore; case EntrySetAction.copy: return context.l10n.collectionActionCopy; case EntrySetAction.move: @@ -143,6 +182,8 @@ extension ExtraEntrySetAction on EntrySetAction { return AIcons.filter; case EntrySetAction.addShortcut: return AIcons.addShortcut; + case EntrySetAction.emptyBin: + return AIcons.emptyBin; // browsing or selecting case EntrySetAction.map: return AIcons.map; @@ -155,6 +196,8 @@ extension ExtraEntrySetAction on EntrySetAction { return AIcons.share; case EntrySetAction.delete: return AIcons.delete; + case EntrySetAction.restore: + return AIcons.restore; case EntrySetAction.copy: return AIcons.copy; case EntrySetAction.move: diff --git a/lib/model/actions/move_type.dart b/lib/model/actions/move_type.dart index 71b326b70..cc7ff0c6b 100644 --- a/lib/model/actions/move_type.dart +++ b/lib/model/actions/move_type.dart @@ -1 +1 @@ -enum MoveType { copy, move, export } +enum MoveType { copy, move, export, toBin, fromBin } diff --git a/lib/model/covers.dart b/lib/model/covers.dart index 2b339889c..3a1acb3b2 100644 --- a/lib/model/covers.dart +++ b/lib/model/covers.dart @@ -23,19 +23,19 @@ class Covers with ChangeNotifier { Set get all => Set.unmodifiable(_rows); - int? coverContentId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.contentId; + int? coverEntryId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.entryId; - Future set(CollectionFilter filter, int? contentId) async { + Future set(CollectionFilter filter, int? entryId) async { // erase contextual properties from filters before saving them if (filter is AlbumFilter) { filter = AlbumFilter(filter.album, null); } _rows.removeWhere((row) => row.filter == filter); - if (contentId == null) { + if (entryId == null) { await metadataDb.removeCovers({filter}); } else { - final row = CoverRow(filter: filter, contentId: contentId); + final row = CoverRow(filter: filter, entryId: entryId); _rows.add(row); await metadataDb.addCovers({row}); } @@ -43,28 +43,26 @@ class Covers with ChangeNotifier { notifyListeners(); } - Future moveEntry(int oldContentId, AvesEntry entry) async { - final oldRows = _rows.where((row) => row.contentId == oldContentId).toSet(); - if (oldRows.isEmpty) return; - - for (final oldRow in oldRows) { - final filter = oldRow.filter; - _rows.remove(oldRow); - if (filter.test(entry)) { - final newRow = CoverRow(filter: filter, contentId: entry.contentId!); - await metadataDb.updateCoverEntryId(oldRow.contentId, newRow); - _rows.add(newRow); - } else { - await metadataDb.removeCovers({filter}); + Future moveEntry(AvesEntry entry, {required bool persist}) async { + final entryId = entry.id; + final rows = _rows.where((row) => row.entryId == entryId).toSet(); + for (final row in rows) { + final filter = row.filter; + if (!filter.test(entry)) { + _rows.remove(row); + if (persist) { + await metadataDb.removeCovers({filter}); + } } } notifyListeners(); } - Future removeEntries(Set entries) async { - final contentIds = entries.map((entry) => entry.contentId).toSet(); - final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet(); + Future removeEntries(Set entries) => removeIds(entries.map((entry) => entry.id).toSet()); + + Future removeIds(Set entryIds) async { + final removedRows = _rows.where((row) => entryIds.contains(row.entryId)).toSet(); await metadataDb.removeCovers(removedRows.map((row) => row.filter).toSet()); _rows.removeAll(removedRows); @@ -85,8 +83,8 @@ class Covers with ChangeNotifier { final visibleEntries = source.visibleEntries; final jsonList = covers.all .map((row) { - final id = row.contentId; - final path = visibleEntries.firstWhereOrNull((entry) => id == entry.contentId)?.path; + final entryId = row.entryId; + final path = visibleEntries.firstWhereOrNull((entry) => entryId == entry.id)?.path; if (path == null) return null; final volume = androidFileUtils.getStorageVolume(path)?.path; @@ -124,7 +122,7 @@ class Covers with ChangeNotifier { final path = pContext.join(volume, relativePath); final entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path && filter.test(entry)); if (entry != null) { - covers.set(filter, entry.contentId); + covers.set(filter, entry.id); } else { debugPrint('failed to import cover for path=$path, filter=$filter'); } @@ -138,14 +136,14 @@ class Covers with ChangeNotifier { @immutable class CoverRow extends Equatable { final CollectionFilter filter; - final int contentId; + final int entryId; @override - List get props => [filter, contentId]; + List get props => [filter, entryId]; const CoverRow({ required this.filter, - required this.contentId, + required this.entryId, }); static CoverRow? fromMap(Map map) { @@ -153,12 +151,12 @@ class CoverRow extends Equatable { if (filter == null) return null; return CoverRow( filter: filter, - contentId: map['contentId'], + entryId: map['entryId'], ); } Map toMap() => { 'filter': filter.toJson(), - 'contentId': contentId, + 'entryId': entryId, }; } diff --git a/lib/model/db/db_metadata.dart b/lib/model/db/db_metadata.dart new file mode 100644 index 000000000..df051677a --- /dev/null +++ b/lib/model/db/db_metadata.dart @@ -0,0 +1,108 @@ +import 'package:aves/model/covers.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/favourites.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/metadata/address.dart'; +import 'package:aves/model/metadata/catalog.dart'; +import 'package:aves/model/metadata/trash.dart'; +import 'package:aves/model/video_playback.dart'; + +abstract class MetadataDb { + int get nextId; + + Future init(); + + Future dbFileSize(); + + Future reset(); + + Future removeIds(Iterable ids, {Set? dataTypes}); + + // entries + + Future clearEntries(); + + Future> loadEntries({String? directory}); + + Future> loadEntriesById(Iterable ids); + + Future saveEntries(Iterable entries); + + Future updateEntry(int id, AvesEntry entry); + + Future> searchEntries(String query, {int? limit}); + + // date taken + + Future clearDates(); + + Future> loadDates(); + + // catalog metadata + + Future clearCatalogMetadata(); + + Future> loadCatalogMetadata(); + + Future> loadCatalogMetadataById(Iterable ids); + + Future saveCatalogMetadata(Set metadataEntries); + + Future updateCatalogMetadata(int id, CatalogMetadata? metadata); + + // address + + Future clearAddresses(); + + Future> loadAddresses(); + + Future> loadAddressesById(Iterable ids); + + Future saveAddresses(Set addresses); + + Future updateAddress(int id, AddressDetails? address); + + // trash + + Future clearTrashDetails(); + + Future> loadAllTrashDetails(); + + Future updateTrash(int id, TrashDetails? details); + + // favourites + + Future clearFavourites(); + + Future> loadAllFavourites(); + + Future addFavourites(Iterable rows); + + Future updateFavouriteId(int id, FavouriteRow row); + + Future removeFavourites(Iterable rows); + + // covers + + Future clearCovers(); + + Future> loadAllCovers(); + + Future addCovers(Iterable rows); + + Future updateCoverEntryId(int id, CoverRow row); + + Future removeCovers(Set filters); + + // video playback + + Future clearVideoPlayback(); + + Future> loadAllVideoPlayback(); + + Future loadVideoPlayback(int? id); + + Future addVideoPlayback(Set rows); + + Future removeVideoPlayback(Iterable ids); +} diff --git a/lib/model/metadata_db.dart b/lib/model/db/db_metadata_sqflite.dart similarity index 55% rename from lib/model/metadata_db.dart rename to lib/model/db/db_metadata_sqflite.dart index 2dfbe54ec..e5133647c 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/db/db_metadata_sqflite.dart @@ -1,106 +1,22 @@ import 'dart:io'; import 'package:aves/model/covers.dart'; +import 'package:aves/model/db/db_metadata.dart'; +import 'package:aves/model/db/db_metadata_sqflite_upgrade.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; -import 'package:aves/model/metadata_db_upgrade.dart'; +import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/video_playback.dart'; import 'package:aves/services/common/services.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; -abstract class MetadataDb { - Future init(); - - Future dbFileSize(); - - Future reset(); - - Future removeIds(Set contentIds, {Set? dataTypes}); - - // entries - - Future clearEntries(); - - Future> loadAllEntries(); - - Future saveEntries(Iterable entries); - - Future updateEntryId(int oldId, AvesEntry entry); - - Future> searchEntries(String query, {int? limit}); - - // date taken - - Future clearDates(); - - Future> loadDates(); - - // catalog metadata - - Future clearMetadataEntries(); - - Future> loadAllMetadataEntries(); - - Future saveMetadata(Set metadataEntries); - - Future updateMetadataId(int oldId, CatalogMetadata? metadata); - - // address - - Future clearAddresses(); - - Future> loadAllAddresses(); - - Future saveAddresses(Set addresses); - - Future updateAddressId(int oldId, AddressDetails? address); - - // favourites - - Future clearFavourites(); - - Future> loadAllFavourites(); - - Future addFavourites(Iterable rows); - - Future updateFavouriteId(int oldId, FavouriteRow row); - - Future removeFavourites(Iterable rows); - - // covers - - Future clearCovers(); - - Future> loadAllCovers(); - - Future addCovers(Iterable rows); - - Future updateCoverEntryId(int oldId, CoverRow row); - - Future removeCovers(Set filters); - - // video playback - - Future clearVideoPlayback(); - - Future> loadAllVideoPlayback(); - - Future loadVideoPlayback(int? contentId); - - Future addVideoPlayback(Set rows); - - Future updateVideoPlaybackId(int oldId, int? newId); - - Future removeVideoPlayback(Set contentIds); -} - class SqfliteMetadataDb implements MetadataDb { - late Future _database; + late Database _db; Future get path async => pContext.join(await getDatabasesPath(), 'metadata.db'); @@ -110,15 +26,22 @@ class SqfliteMetadataDb implements MetadataDb { static const addressTable = 'address'; static const favouriteTable = 'favourites'; static const coverTable = 'covers'; + static const trashTable = 'trash'; static const videoPlaybackTable = 'videoPlayback'; + static int _lastId = 0; + + @override + int get nextId => ++_lastId; + @override Future init() async { - _database = openDatabase( + _db = await openDatabase( await path, onCreate: (db, version) async { await db.execute('CREATE TABLE $entryTable(' - 'contentId INTEGER PRIMARY KEY' + 'id INTEGER PRIMARY KEY' + ', contentId INTEGER' ', uri TEXT' ', path TEXT' ', sourceMimeType TEXT' @@ -130,13 +53,14 @@ class SqfliteMetadataDb implements MetadataDb { ', dateModifiedSecs INTEGER' ', sourceDateTakenMillis INTEGER' ', durationMillis INTEGER' + ', trashed INTEGER DEFAULT 0' ')'); await db.execute('CREATE TABLE $dateTakenTable(' - 'contentId INTEGER PRIMARY KEY' + 'id INTEGER PRIMARY KEY' ', dateMillis INTEGER' ')'); await db.execute('CREATE TABLE $metadataTable(' - 'contentId INTEGER PRIMARY KEY' + 'id INTEGER PRIMARY KEY' ', mimeType TEXT' ', dateMillis INTEGER' ', flags INTEGER' @@ -148,7 +72,7 @@ class SqfliteMetadataDb implements MetadataDb { ', rating INTEGER' ')'); await db.execute('CREATE TABLE $addressTable(' - 'contentId INTEGER PRIMARY KEY' + 'id INTEGER PRIMARY KEY' ', addressLine TEXT' ', countryCode TEXT' ', countryName TEXT' @@ -156,21 +80,28 @@ class SqfliteMetadataDb implements MetadataDb { ', locality TEXT' ')'); await db.execute('CREATE TABLE $favouriteTable(' - 'contentId INTEGER PRIMARY KEY' - ', path TEXT' + 'id INTEGER PRIMARY KEY' ')'); await db.execute('CREATE TABLE $coverTable(' 'filter TEXT PRIMARY KEY' - ', contentId INTEGER' + ', entryId INTEGER' + ')'); + await db.execute('CREATE TABLE $trashTable(' + 'id INTEGER PRIMARY KEY' + ', path TEXT' + ', dateMillis INTEGER' ')'); await db.execute('CREATE TABLE $videoPlaybackTable(' - 'contentId INTEGER PRIMARY KEY' + 'id INTEGER PRIMARY KEY' ', resumeTimeMillis INTEGER' ')'); }, onUpgrade: MetadataDbUpgrader.upgradeDb, - version: 6, + version: 7, ); + + final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable'); + _lastId = (maxIdRows.firstOrNull?['maxId'] as int?) ?? 0; } @override @@ -182,22 +113,22 @@ class SqfliteMetadataDb implements MetadataDb { @override Future reset() async { debugPrint('$runtimeType reset'); - await (await _database).close(); + await _db.close(); await deleteDatabase(await path); await init(); } @override - Future removeIds(Set contentIds, {Set? dataTypes}) async { - if (contentIds.isEmpty) return; + Future removeIds(Iterable ids, {Set? dataTypes}) async { + if (ids.isEmpty) return; final _dataTypes = dataTypes ?? EntryDataType.values.toSet(); - final db = await _database; - // using array in `whereArgs` and using it with `where contentId IN ?` is a pain, so we prefer `batch` instead - final batch = db.batch(); - const where = 'contentId = ?'; - contentIds.forEach((id) { + // using array in `whereArgs` and using it with `where id IN ?` is a pain, so we prefer `batch` instead + final batch = _db.batch(); + const where = 'id = ?'; + const coverWhere = 'entryId = ?'; + ids.forEach((id) { final whereArgs = [id]; if (_dataTypes.contains(EntryDataType.basic)) { batch.delete(entryTable, where: where, whereArgs: whereArgs); @@ -211,7 +142,8 @@ class SqfliteMetadataDb implements MetadataDb { } if (_dataTypes.contains(EntryDataType.references)) { batch.delete(favouriteTable, where: where, whereArgs: whereArgs); - batch.delete(coverTable, where: where, whereArgs: whereArgs); + batch.delete(coverTable, where: coverWhere, whereArgs: whereArgs); + batch.delete(trashTable, where: where, whereArgs: whereArgs); batch.delete(videoPlaybackTable, where: where, whereArgs: whereArgs); } }); @@ -222,35 +154,54 @@ class SqfliteMetadataDb implements MetadataDb { @override Future clearEntries() async { - final db = await _database; - final count = await db.delete(entryTable, where: '1'); + final count = await _db.delete(entryTable, where: '1'); debugPrint('$runtimeType clearEntries deleted $count rows'); } @override - Future> loadAllEntries() async { - final db = await _database; - final maps = await db.query(entryTable); - final entries = maps.map(AvesEntry.fromMap).toSet(); - return entries; + Future> loadEntries({String? directory}) async { + if (directory != null) { + final separator = pContext.separator; + if (!directory.endsWith(separator)) { + directory = '$directory$separator'; + } + + const where = 'path LIKE ?'; + final whereArgs = ['$directory%']; + final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs); + + final dirLength = directory.length; + return rows + .whereNot((row) { + // skip entries in subfolders + final path = row['path'] as String?; + return path == null || path.substring(dirLength).contains(separator); + }) + .map(AvesEntry.fromMap) + .toSet(); + } + + final rows = await _db.query(entryTable); + return rows.map(AvesEntry.fromMap).toSet(); } + @override + Future> loadEntriesById(Iterable ids) => _getByIds(ids, entryTable, AvesEntry.fromMap); + @override Future saveEntries(Iterable entries) async { if (entries.isEmpty) return; final stopwatch = Stopwatch()..start(); - final db = await _database; - final batch = db.batch(); + final batch = _db.batch(); entries.forEach((entry) => _batchInsertEntry(batch, entry)); await batch.commit(noResult: true); debugPrint('$runtimeType saveEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries'); } @override - Future updateEntryId(int oldId, AvesEntry entry) async { - final db = await _database; - final batch = db.batch(); - batch.delete(entryTable, where: 'contentId = ?', whereArgs: [oldId]); + Future updateEntry(int id, AvesEntry entry) async { + final batch = _db.batch(); + batch.delete(entryTable, where: 'id = ?', whereArgs: [id]); _batchInsertEntry(batch, entry); await batch.commit(noResult: true); } @@ -265,58 +216,53 @@ class SqfliteMetadataDb implements MetadataDb { @override Future> searchEntries(String query, {int? limit}) async { - final db = await _database; - final maps = await db.query( + final rows = await _db.query( entryTable, where: 'title LIKE ?', whereArgs: ['%$query%'], orderBy: 'sourceDateTakenMillis DESC', limit: limit, ); - return maps.map(AvesEntry.fromMap).toSet(); + return rows.map(AvesEntry.fromMap).toSet(); } // date taken @override Future clearDates() async { - final db = await _database; - final count = await db.delete(dateTakenTable, where: '1'); + final count = await _db.delete(dateTakenTable, where: '1'); debugPrint('$runtimeType clearDates deleted $count rows'); } @override Future> loadDates() async { - final db = await _database; - final maps = await db.query(dateTakenTable); - final metadataEntries = Map.fromEntries(maps.map((map) => MapEntry(map['contentId'] as int, (map['dateMillis'] ?? 0) as int))); - return metadataEntries; + final rows = await _db.query(dateTakenTable); + return Map.fromEntries(rows.map((map) => MapEntry(map['id'] as int, (map['dateMillis'] ?? 0) as int))); } // catalog metadata @override - Future clearMetadataEntries() async { - final db = await _database; - final count = await db.delete(metadataTable, where: '1'); + Future clearCatalogMetadata() async { + final count = await _db.delete(metadataTable, where: '1'); debugPrint('$runtimeType clearMetadataEntries deleted $count rows'); } @override - Future> loadAllMetadataEntries() async { - final db = await _database; - final maps = await db.query(metadataTable); - final metadataEntries = maps.map(CatalogMetadata.fromMap).toList(); - return metadataEntries; + Future> loadCatalogMetadata() async { + final rows = await _db.query(metadataTable); + return rows.map(CatalogMetadata.fromMap).toSet(); } @override - Future saveMetadata(Set metadataEntries) async { + Future> loadCatalogMetadataById(Iterable ids) => _getByIds(ids, metadataTable, CatalogMetadata.fromMap); + + @override + Future saveCatalogMetadata(Set metadataEntries) async { if (metadataEntries.isEmpty) return; final stopwatch = Stopwatch()..start(); try { - final db = await _database; - final batch = db.batch(); + final batch = _db.batch(); metadataEntries.forEach((metadata) => _batchInsertMetadata(batch, metadata)); await batch.commit(noResult: true); debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); @@ -326,11 +272,10 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future updateMetadataId(int oldId, CatalogMetadata? metadata) async { - final db = await _database; - final batch = db.batch(); - batch.delete(dateTakenTable, where: 'contentId = ?', whereArgs: [oldId]); - batch.delete(metadataTable, where: 'contentId = ?', whereArgs: [oldId]); + Future updateCatalogMetadata(int id, CatalogMetadata? metadata) async { + final batch = _db.batch(); + batch.delete(dateTakenTable, where: 'id = ?', whereArgs: [id]); + batch.delete(metadataTable, where: 'id = ?', whereArgs: [id]); _batchInsertMetadata(batch, metadata); await batch.commit(noResult: true); } @@ -341,7 +286,7 @@ class SqfliteMetadataDb implements MetadataDb { batch.insert( dateTakenTable, { - 'contentId': metadata.contentId, + 'id': metadata.id, 'dateMillis': metadata.dateMillis, }, conflictAlgorithm: ConflictAlgorithm.replace, @@ -358,35 +303,33 @@ class SqfliteMetadataDb implements MetadataDb { @override Future clearAddresses() async { - final db = await _database; - final count = await db.delete(addressTable, where: '1'); + final count = await _db.delete(addressTable, where: '1'); debugPrint('$runtimeType clearAddresses deleted $count rows'); } @override - Future> loadAllAddresses() async { - final db = await _database; - final maps = await db.query(addressTable); - final addresses = maps.map(AddressDetails.fromMap).toList(); - return addresses; + Future> loadAddresses() async { + final rows = await _db.query(addressTable); + return rows.map(AddressDetails.fromMap).toSet(); } + @override + Future> loadAddressesById(Iterable ids) => _getByIds(ids, addressTable, AddressDetails.fromMap); + @override Future saveAddresses(Set addresses) async { if (addresses.isEmpty) return; final stopwatch = Stopwatch()..start(); - final db = await _database; - final batch = db.batch(); + final batch = _db.batch(); addresses.forEach((address) => _batchInsertAddress(batch, address)); await batch.commit(noResult: true); debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries'); } @override - Future updateAddressId(int oldId, AddressDetails? address) async { - final db = await _database; - final batch = db.batch(); - batch.delete(addressTable, where: 'contentId = ?', whereArgs: [oldId]); + Future updateAddress(int id, AddressDetails? address) async { + final batch = _db.batch(); + batch.delete(addressTable, where: 'id = ?', whereArgs: [id]); _batchInsertAddress(batch, address); await batch.commit(noResult: true); } @@ -400,37 +343,63 @@ class SqfliteMetadataDb implements MetadataDb { ); } + // trash + + @override + Future clearTrashDetails() async { + final count = await _db.delete(trashTable, where: '1'); + debugPrint('$runtimeType clearTrashDetails deleted $count rows'); + } + + @override + Future> loadAllTrashDetails() async { + final rows = await _db.query(trashTable); + return rows.map(TrashDetails.fromMap).toSet(); + } + + @override + Future updateTrash(int id, TrashDetails? details) async { + final batch = _db.batch(); + batch.delete(trashTable, where: 'id = ?', whereArgs: [id]); + _batchInsertTrashDetails(batch, details); + await batch.commit(noResult: true); + } + + void _batchInsertTrashDetails(Batch batch, TrashDetails? details) { + if (details == null) return; + batch.insert( + trashTable, + details.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + // favourites @override Future clearFavourites() async { - final db = await _database; - final count = await db.delete(favouriteTable, where: '1'); + final count = await _db.delete(favouriteTable, where: '1'); debugPrint('$runtimeType clearFavourites deleted $count rows'); } @override Future> loadAllFavourites() async { - final db = await _database; - final maps = await db.query(favouriteTable); - final rows = maps.map(FavouriteRow.fromMap).toSet(); - return rows; + final rows = await _db.query(favouriteTable); + return rows.map(FavouriteRow.fromMap).toSet(); } @override Future addFavourites(Iterable rows) async { if (rows.isEmpty) return; - final db = await _database; - final batch = db.batch(); + final batch = _db.batch(); rows.forEach((row) => _batchInsertFavourite(batch, row)); await batch.commit(noResult: true); } @override - Future updateFavouriteId(int oldId, FavouriteRow row) async { - final db = await _database; - final batch = db.batch(); - batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [oldId]); + Future updateFavouriteId(int id, FavouriteRow row) async { + final batch = _db.batch(); + batch.delete(favouriteTable, where: 'id = ?', whereArgs: [id]); _batchInsertFavourite(batch, row); await batch.commit(noResult: true); } @@ -446,13 +415,12 @@ class SqfliteMetadataDb implements MetadataDb { @override Future removeFavourites(Iterable rows) async { if (rows.isEmpty) return; - final ids = rows.map((row) => row.contentId); + final ids = rows.map((row) => row.entryId); if (ids.isEmpty) return; - final db = await _database; - // using array in `whereArgs` and using it with `where contentId IN ?` is a pain, so we prefer `batch` instead - final batch = db.batch(); - ids.forEach((id) => batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [id])); + // using array in `whereArgs` and using it with `where id IN ?` is a pain, so we prefer `batch` instead + final batch = _db.batch(); + ids.forEach((id) => batch.delete(favouriteTable, where: 'id = ?', whereArgs: [id])); await batch.commit(noResult: true); } @@ -460,34 +428,29 @@ class SqfliteMetadataDb implements MetadataDb { @override Future clearCovers() async { - final db = await _database; - final count = await db.delete(coverTable, where: '1'); + final count = await _db.delete(coverTable, where: '1'); debugPrint('$runtimeType clearCovers deleted $count rows'); } @override Future> loadAllCovers() async { - final db = await _database; - final maps = await db.query(coverTable); - final rows = maps.map(CoverRow.fromMap).whereNotNull().toSet(); - return rows; + final rows = await _db.query(coverTable); + return rows.map(CoverRow.fromMap).whereNotNull().toSet(); } @override Future addCovers(Iterable rows) async { if (rows.isEmpty) return; - final db = await _database; - final batch = db.batch(); + final batch = _db.batch(); rows.forEach((row) => _batchInsertCover(batch, row)); await batch.commit(noResult: true); } @override - Future updateCoverEntryId(int oldId, CoverRow row) async { - final db = await _database; - final batch = db.batch(); - batch.delete(coverTable, where: 'contentId = ?', whereArgs: [oldId]); + Future updateCoverEntryId(int id, CoverRow row) async { + final batch = _db.batch(); + batch.delete(coverTable, where: 'entryId = ?', whereArgs: [id]); _batchInsertCover(batch, row); await batch.commit(noResult: true); } @@ -504,9 +467,8 @@ class SqfliteMetadataDb implements MetadataDb { Future removeCovers(Set filters) async { if (filters.isEmpty) return; - final db = await _database; // using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead - final batch = db.batch(); + final batch = _db.batch(); filters.forEach((filter) => batch.delete(coverTable, where: 'filter = ?', whereArgs: [filter.toJson()])); await batch.commit(noResult: true); } @@ -515,36 +477,31 @@ class SqfliteMetadataDb implements MetadataDb { @override Future clearVideoPlayback() async { - final db = await _database; - final count = await db.delete(videoPlaybackTable, where: '1'); + final count = await _db.delete(videoPlaybackTable, where: '1'); debugPrint('$runtimeType clearVideoPlayback deleted $count rows'); } @override Future> loadAllVideoPlayback() async { - final db = await _database; - final maps = await db.query(videoPlaybackTable); - final rows = maps.map(VideoPlaybackRow.fromMap).whereNotNull().toSet(); - return rows; + final rows = await _db.query(videoPlaybackTable); + return rows.map(VideoPlaybackRow.fromMap).whereNotNull().toSet(); } @override - Future loadVideoPlayback(int? contentId) async { - if (contentId == null) return null; + Future loadVideoPlayback(int? id) async { + if (id == null) return null; - final db = await _database; - final maps = await db.query(videoPlaybackTable, where: 'contentId = ?', whereArgs: [contentId]); - if (maps.isEmpty) return null; + final rows = await _db.query(videoPlaybackTable, where: 'id = ?', whereArgs: [id]); + if (rows.isEmpty) return null; - return VideoPlaybackRow.fromMap(maps.first); + return VideoPlaybackRow.fromMap(rows.first); } @override Future addVideoPlayback(Set rows) async { if (rows.isEmpty) return; - final db = await _database; - final batch = db.batch(); + final batch = _db.batch(); rows.forEach((row) => _batchInsertVideoPlayback(batch, row)); await batch.commit(noResult: true); } @@ -558,23 +515,23 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future updateVideoPlaybackId(int oldId, int? newId) async { - if (newId != null) { - final db = await _database; - await db.update(videoPlaybackTable, {'contentId': newId}, where: 'contentId = ?', whereArgs: [oldId]); - } else { - await removeVideoPlayback({oldId}); - } - } + Future removeVideoPlayback(Iterable ids) async { + if (ids.isEmpty) return; - @override - Future removeVideoPlayback(Set contentIds) async { - if (contentIds.isEmpty) return; - - final db = await _database; // using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead - final batch = db.batch(); - contentIds.forEach((id) => batch.delete(videoPlaybackTable, where: 'contentId = ?', whereArgs: [id])); + final batch = _db.batch(); + ids.forEach((id) => batch.delete(videoPlaybackTable, where: 'id = ?', whereArgs: [id])); await batch.commit(noResult: true); } + + // convenience methods + + Future> _getByIds(Iterable ids, String table, T Function(Map row) mapRow) async { + if (ids.isEmpty) return {}; + final rows = await _db.query( + table, + where: 'id IN (${ids.join(',')})', + ); + return rows.map(mapRow).toSet(); + } } diff --git a/lib/model/db/db_metadata_sqflite_upgrade.dart b/lib/model/db/db_metadata_sqflite_upgrade.dart new file mode 100644 index 000000000..c818e6c18 --- /dev/null +++ b/lib/model/db/db_metadata_sqflite_upgrade.dart @@ -0,0 +1,272 @@ +import 'package:aves/model/db/db_metadata_sqflite.dart'; +import 'package:flutter/foundation.dart'; +import 'package:sqflite/sqflite.dart'; + +class MetadataDbUpgrader { + static const entryTable = SqfliteMetadataDb.entryTable; + static const dateTakenTable = SqfliteMetadataDb.dateTakenTable; + static const metadataTable = SqfliteMetadataDb.metadataTable; + static const addressTable = SqfliteMetadataDb.addressTable; + static const favouriteTable = SqfliteMetadataDb.favouriteTable; + static const coverTable = SqfliteMetadataDb.coverTable; + static const trashTable = SqfliteMetadataDb.trashTable; + static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable; + + // warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported + // on SQLite <3.25.0, bundled on older Android devices + static Future upgradeDb(Database db, int oldVersion, int newVersion) async { + while (oldVersion < newVersion) { + switch (oldVersion) { + case 1: + await _upgradeFrom1(db); + break; + case 2: + await _upgradeFrom2(db); + break; + case 3: + await _upgradeFrom3(db); + break; + case 4: + await _upgradeFrom4(db); + break; + case 5: + await _upgradeFrom5(db); + break; + case 6: + await _upgradeFrom6(db); + break; + } + oldVersion++; + } + } + + static Future _upgradeFrom1(Database db) async { + debugPrint('upgrading DB from v1'); + // rename column 'orientationDegrees' to 'sourceRotationDegrees' + await db.transaction((txn) async { + const newEntryTable = '${entryTable}TEMP'; + await db.execute('CREATE TABLE $newEntryTable(' + 'contentId INTEGER PRIMARY KEY' + ', uri TEXT' + ', path TEXT' + ', sourceMimeType TEXT' + ', width INTEGER' + ', height INTEGER' + ', sourceRotationDegrees INTEGER' + ', sizeBytes INTEGER' + ', title TEXT' + ', dateModifiedSecs INTEGER' + ', sourceDateTakenMillis INTEGER' + ', durationMillis INTEGER' + ')'); + await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)' + ' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis' + ' FROM $entryTable;'); + await db.execute('DROP TABLE $entryTable;'); + await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;'); + }); + + // rename column 'videoRotation' to 'rotationDegrees' + await db.transaction((txn) async { + const newMetadataTable = '${metadataTable}TEMP'; + await db.execute('CREATE TABLE $newMetadataTable(' + 'contentId INTEGER PRIMARY KEY' + ', mimeType TEXT' + ', dateMillis INTEGER' + ', isAnimated INTEGER' + ', rotationDegrees INTEGER' + ', xmpSubjects TEXT' + ', xmpTitleDescription TEXT' + ', latitude REAL' + ', longitude REAL' + ')'); + await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,isAnimated,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)' + ' SELECT contentId,mimeType,dateMillis,isAnimated,videoRotation,xmpSubjects,xmpTitleDescription,latitude,longitude' + ' FROM $metadataTable;'); + await db.rawInsert('UPDATE $newMetadataTable SET rotationDegrees = NULL WHERE rotationDegrees = 0;'); + await db.execute('DROP TABLE $metadataTable;'); + await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); + }); + + // new column 'isFlipped' + await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;'); + } + + static Future _upgradeFrom2(Database db) async { + debugPrint('upgrading DB from v2'); + // merge columns 'isAnimated' and 'isFlipped' into 'flags' + await db.transaction((txn) async { + const newMetadataTable = '${metadataTable}TEMP'; + await db.execute('CREATE TABLE $newMetadataTable(' + 'contentId INTEGER PRIMARY KEY' + ', mimeType TEXT' + ', dateMillis INTEGER' + ', flags INTEGER' + ', rotationDegrees INTEGER' + ', xmpSubjects TEXT' + ', xmpTitleDescription TEXT' + ', latitude REAL' + ', longitude REAL' + ')'); + await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)' + ' SELECT contentId,mimeType,dateMillis,ifnull(isAnimated,0)+ifnull(isFlipped,0)*2,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude' + ' FROM $metadataTable;'); + await db.execute('DROP TABLE $metadataTable;'); + await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); + }); + } + + static Future _upgradeFrom3(Database db) async { + debugPrint('upgrading DB from v3'); + await db.execute('CREATE TABLE $coverTable(' + 'filter TEXT PRIMARY KEY' + ', contentId INTEGER' + ')'); + } + + static Future _upgradeFrom4(Database db) async { + debugPrint('upgrading DB from v4'); + await db.execute('CREATE TABLE $videoPlaybackTable(' + 'contentId INTEGER PRIMARY KEY' + ', resumeTimeMillis INTEGER' + ')'); + } + + static Future _upgradeFrom5(Database db) async { + debugPrint('upgrading DB from v5'); + await db.execute('ALTER TABLE $metadataTable ADD COLUMN rating INTEGER;'); + } + + static Future _upgradeFrom6(Database db) async { + debugPrint('upgrading DB from v6'); + // new primary key column `id` instead of `contentId` + // new column `trashed` + await db.transaction((txn) async { + const newEntryTable = '${entryTable}TEMP'; + await db.execute('CREATE TABLE $newEntryTable(' + 'id INTEGER PRIMARY KEY' + ', contentId INTEGER' + ', uri TEXT' + ', path TEXT' + ', sourceMimeType TEXT' + ', width INTEGER' + ', height INTEGER' + ', sourceRotationDegrees INTEGER' + ', sizeBytes INTEGER' + ', title TEXT' + ', dateModifiedSecs INTEGER' + ', sourceDateTakenMillis INTEGER' + ', durationMillis INTEGER' + ', trashed INTEGER DEFAULT 0' + ')'); + await db.rawInsert('INSERT INTO $newEntryTable(id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)' + ' SELECT contentId,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis' + ' FROM $entryTable;'); + await db.execute('DROP TABLE $entryTable;'); + await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;'); + }); + + // rename column `contentId` to `id` + await db.transaction((txn) async { + const newDateTakenTable = '${dateTakenTable}TEMP'; + await db.execute('CREATE TABLE $newDateTakenTable(' + 'id INTEGER PRIMARY KEY' + ', dateMillis INTEGER' + ')'); + await db.rawInsert('INSERT INTO $newDateTakenTable(id,dateMillis)' + ' SELECT contentId,dateMillis' + ' FROM $dateTakenTable;'); + await db.execute('DROP TABLE $dateTakenTable;'); + await db.execute('ALTER TABLE $newDateTakenTable RENAME TO $dateTakenTable;'); + }); + + // rename column `contentId` to `id` + await db.transaction((txn) async { + const newMetadataTable = '${metadataTable}TEMP'; + await db.execute('CREATE TABLE $newMetadataTable(' + 'id INTEGER PRIMARY KEY' + ', mimeType TEXT' + ', dateMillis INTEGER' + ', flags INTEGER' + ', rotationDegrees INTEGER' + ', xmpSubjects TEXT' + ', xmpTitleDescription TEXT' + ', latitude REAL' + ', longitude REAL' + ', rating INTEGER' + ')'); + await db.rawInsert('INSERT INTO $newMetadataTable(id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating)' + ' SELECT contentId,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating' + ' FROM $metadataTable;'); + await db.execute('DROP TABLE $metadataTable;'); + await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); + }); + + // rename column `contentId` to `id` + await db.transaction((txn) async { + const newAddressTable = '${addressTable}TEMP'; + await db.execute('CREATE TABLE $newAddressTable(' + 'id INTEGER PRIMARY KEY' + ', addressLine TEXT' + ', countryCode TEXT' + ', countryName TEXT' + ', adminArea TEXT' + ', locality TEXT' + ')'); + await db.rawInsert('INSERT INTO $newAddressTable(id,addressLine,countryCode,countryName,adminArea,locality)' + ' SELECT contentId,addressLine,countryCode,countryName,adminArea,locality' + ' FROM $addressTable;'); + await db.execute('DROP TABLE $addressTable;'); + await db.execute('ALTER TABLE $newAddressTable RENAME TO $addressTable;'); + }); + + // rename column `contentId` to `id` + await db.transaction((txn) async { + const newVideoPlaybackTable = '${videoPlaybackTable}TEMP'; + await db.execute('CREATE TABLE $newVideoPlaybackTable(' + 'id INTEGER PRIMARY KEY' + ', resumeTimeMillis INTEGER' + ')'); + await db.rawInsert('INSERT INTO $newVideoPlaybackTable(id,resumeTimeMillis)' + ' SELECT contentId,resumeTimeMillis' + ' FROM $videoPlaybackTable;'); + await db.execute('DROP TABLE $videoPlaybackTable;'); + await db.execute('ALTER TABLE $newVideoPlaybackTable RENAME TO $videoPlaybackTable;'); + }); + + // rename column `contentId` to `id` + // remove column `path` + await db.transaction((txn) async { + const newFavouriteTable = '${favouriteTable}TEMP'; + await db.execute('CREATE TABLE $newFavouriteTable(' + 'id INTEGER PRIMARY KEY' + ')'); + await db.rawInsert('INSERT INTO $newFavouriteTable(id)' + ' SELECT contentId' + ' FROM $favouriteTable;'); + await db.execute('DROP TABLE $favouriteTable;'); + await db.execute('ALTER TABLE $newFavouriteTable RENAME TO $favouriteTable;'); + }); + + // rename column `contentId` to `entryId` + await db.transaction((txn) async { + const newCoverTable = '${coverTable}TEMP'; + await db.execute('CREATE TABLE $newCoverTable(' + 'filter TEXT PRIMARY KEY' + ', entryId INTEGER' + ')'); + await db.rawInsert('INSERT INTO $newCoverTable(filter,entryId)' + ' SELECT filter,contentId' + ' FROM $coverTable;'); + await db.execute('DROP TABLE $coverTable;'); + await db.execute('ALTER TABLE $newCoverTable RENAME TO $coverTable;'); + }); + + // new table + await db.execute('CREATE TABLE $trashTable(' + 'id INTEGER PRIMARY KEY' + ', path TEXT' + ', dateMillis INTEGER' + ')'); + } +} diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 20ee4573b..2e5dedd16 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -7,7 +7,9 @@ import 'package:aves/model/entry_cache.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; +import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/multipage.dart'; +import 'package:aves/model/source/trash.dart'; import 'package:aves/model/video/metadata.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/service_policy.dart'; @@ -25,29 +27,27 @@ import 'package:latlong2/latlong.dart'; enum EntryDataType { basic, catalog, address, references } class AvesEntry { + // `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode + int id; String uri; - String? _path, _directory, _filename, _extension; + String? _path, _directory, _filename, _extension, _sourceTitle; int? pageId, contentId; final String sourceMimeType; - int width; - int height; - int sourceRotationDegrees; - int? sizeBytes; - String? _sourceTitle; + int width, height, sourceRotationDegrees; + int? sizeBytes, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; + bool trashed; - // `dateModifiedSecs` can be missing in viewer mode - int? _dateModifiedSecs; - int? sourceDateTakenMillis; - int? _durationMillis; int? _catalogDateMillis; CatalogMetadata? _catalogMetadata; AddressDetails? _addressDetails; + TrashDetails? trashDetails; List? burstEntries; final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier(); AvesEntry({ + required int? id, required this.uri, required String? path, required this.contentId, @@ -61,8 +61,9 @@ class AvesEntry { required int? dateModifiedSecs, required this.sourceDateTakenMillis, required int? durationMillis, + required this.trashed, this.burstEntries, - }) { + }) : id = id ?? 0 { this.path = path; this.sourceTitle = sourceTitle; this.dateModifiedSecs = dateModifiedSecs; @@ -74,6 +75,7 @@ class AvesEntry { bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType); AvesEntry copyWith({ + int? id, String? uri, String? path, int? contentId, @@ -81,11 +83,12 @@ class AvesEntry { int? dateModifiedSecs, List? burstEntries, }) { - final copyContentId = contentId ?? this.contentId; + final copyEntryId = id ?? this.id; final copied = AvesEntry( + id: copyEntryId, uri: uri ?? this.uri, path: path ?? this.path, - contentId: copyContentId, + contentId: contentId ?? this.contentId, pageId: null, sourceMimeType: sourceMimeType, width: width, @@ -96,10 +99,12 @@ class AvesEntry { dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs, sourceDateTakenMillis: sourceDateTakenMillis, durationMillis: durationMillis, + trashed: trashed, burstEntries: burstEntries ?? this.burstEntries, ) - ..catalogMetadata = _catalogMetadata?.copyWith(contentId: copyContentId) - ..addressDetails = _addressDetails?.copyWith(contentId: copyContentId); + ..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId) + ..addressDetails = _addressDetails?.copyWith(id: copyEntryId) + ..trashDetails = trashDetails?.copyWith(id: copyEntryId); return copied; } @@ -107,6 +112,7 @@ class AvesEntry { // from DB or platform source entry factory AvesEntry.fromMap(Map map) { return AvesEntry( + id: map['id'] as int?, uri: map['uri'] as String, path: map['path'] as String?, pageId: null, @@ -120,12 +126,14 @@ class AvesEntry { dateModifiedSecs: map['dateModifiedSecs'] as int?, sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?, durationMillis: map['durationMillis'] as int?, + trashed: (map['trashed'] as int? ?? 0) != 0, ); } // for DB only Map toMap() { return { + 'id': id, 'uri': uri, 'path': path, 'contentId': contentId, @@ -138,6 +146,7 @@ class AvesEntry { 'dateModifiedSecs': dateModifiedSecs, 'sourceDateTakenMillis': sourceDateTakenMillis, 'durationMillis': durationMillis, + 'trashed': trashed ? 1 : 0, }; } @@ -151,7 +160,7 @@ class AvesEntry { // so that we can reliably use instances in a `Set`, which requires consistent hash codes over time @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}'; + String toString() => '$runtimeType#${shortHash(this)}{id=$id, uri=$uri, path=$path, pageId=$pageId}'; set path(String? path) { _path = path; @@ -179,7 +188,10 @@ class AvesEntry { return _extension; } - bool get isMissingAtPath => path != null && !File(path!).existsSync(); + bool get isMissingAtPath { + final effectivePath = trashed ? trashDetails?.path : path; + return effectivePath != null && !File(effectivePath).existsSync(); + } // the MIME type reported by the Media Store is unreliable // so we use the one found during cataloguing if possible @@ -233,7 +245,7 @@ class AvesEntry { bool get is360 => _catalogMetadata?.is360 ?? false; - bool get canEdit => path != null; + bool get canEdit => path != null && !trashed; bool get canEditDate => canEdit && (canEditExif || canEditXmp); @@ -408,6 +420,18 @@ class AvesEntry { return _durationText!; } + bool get isExpiredTrash { + final dateMillis = trashDetails?.dateMillis; + if (dateMillis == null) return false; + return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).isBefore(DateTime.now()); + } + + int? get trashDaysLeft { + final dateMillis = trashDetails?.dateMillis; + if (dateMillis == null) return null; + return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays; + } + // returns whether this entry has GPS coordinates // (0, 0) coordinates are considered invalid, as it is likely a default value bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0; @@ -476,7 +500,7 @@ class AvesEntry { }; await applyNewFields(fields, persist: persist); } - catalogMetadata = CatalogMetadata(contentId: contentId); + catalogMetadata = CatalogMetadata(id: id); } else { if (isVideo && (!isSized || durationMillis == 0)) { // exotic video that is not sized during loading @@ -519,7 +543,7 @@ class AvesEntry { void setCountry(CountryCode? countryCode) { if (hasFineAddress || countryCode == null) return; addressDetails = AddressDetails( - contentId: contentId, + id: id, countryCode: countryCode.alpha2, countryName: countryCode.alpha3, ); @@ -542,7 +566,7 @@ class AvesEntry { final cn = address.countryName; final aa = address.adminArea; addressDetails = AddressDetails( - contentId: contentId, + id: id, countryCode: cc, countryName: cn, adminArea: aa, @@ -619,7 +643,7 @@ class AvesEntry { if (persist) { await metadataDb.saveEntries({this}); - if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!}); + if (catalogMetadata != null) await metadataDb.saveCatalogMetadata({catalogMetadata!}); } await _onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); @@ -638,7 +662,7 @@ class AvesEntry { _tags = null; if (persist) { - await metadataDb.removeIds({contentId!}, dataTypes: dataTypes); + await metadataDb.removeIds({id}, dataTypes: dataTypes); } final updatedEntry = await mediaFileService.getEntry(uri, mimeType); @@ -689,7 +713,7 @@ class AvesEntry { Future removeFromFavourites() async { if (isFavourite) { - await favourites.remove({this}); + await favourites.removeEntries({this}); } } @@ -720,7 +744,7 @@ class AvesEntry { pages: burstEntries! .mapIndexed((index, entry) => SinglePageInfo( index: index, - pageId: entry.contentId!, + pageId: entry.id, isDefault: index == 0, uri: entry.uri, mimeType: entry.mimeType, diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_metadata_edition.dart index ad14a3923..2b2ee6054 100644 --- a/lib/model/entry_metadata_edition.dart +++ b/lib/model/entry_metadata_edition.dart @@ -22,7 +22,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { final appliedModifier = await _applyDateModifierToEntry(userModifier); if (appliedModifier == null) { - await reportService.recordError('failed to get date for modifier=$userModifier, uri=$uri', null); + await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null); return {}; } @@ -259,7 +259,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { ); } - // convenience + // convenience methods // This method checks whether the item already has a metadata date, // and adds a date (the file modified date) via Exif if possible. diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart index cc8768f1e..edfb2406e 100644 --- a/lib/model/favourites.dart +++ b/lib/model/favourites.dart @@ -19,11 +19,11 @@ class Favourites with ChangeNotifier { int get count => _rows.length; - Set get all => Set.unmodifiable(_rows.map((v) => v.contentId)); + Set get all => Set.unmodifiable(_rows.map((v) => v.entryId)); - bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId); + bool isFavourite(AvesEntry entry) => _rows.any((row) => row.entryId == entry.id); - FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!); + FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(entryId: entry.id); Future add(Set entries) async { final newRows = entries.map(_entryToRow); @@ -34,9 +34,10 @@ class Favourites with ChangeNotifier { notifyListeners(); } - Future remove(Set entries) async { - final contentIds = entries.map((entry) => entry.contentId).toSet(); - final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet(); + Future removeEntries(Set entries) => removeIds(entries.map((entry) => entry.id).toSet()); + + Future removeIds(Set entryIds) async { + final removedRows = _rows.where((row) => entryIds.contains(row.entryId)).toSet(); await metadataDb.removeFavourites(removedRows); removedRows.forEach(_rows.remove); @@ -44,19 +45,6 @@ class Favourites with ChangeNotifier { notifyListeners(); } - Future moveEntry(int oldContentId, AvesEntry entry) async { - final oldRow = _rows.firstWhereOrNull((row) => row.contentId == oldContentId); - if (oldRow == null) return; - - final newRow = _entryToRow(entry); - - await metadataDb.updateFavouriteId(oldContentId, newRow); - _rows.remove(oldRow); - _rows.add(newRow); - - notifyListeners(); - } - Future clear() async { await metadataDb.clearFavourites(); _rows.clear(); @@ -69,7 +57,7 @@ class Favourites with ChangeNotifier { Map>? export(CollectionSource source) { final visibleEntries = source.visibleEntries; final ids = favourites.all; - final paths = visibleEntries.where((entry) => ids.contains(entry.contentId)).map((entry) => entry.path).whereNotNull().toSet(); + final paths = visibleEntries.where((entry) => ids.contains(entry.id)).map((entry) => entry.path).whereNotNull().toSet(); final byVolume = groupBy(paths, androidFileUtils.getStorageVolume); final jsonMap = Map.fromEntries(byVolume.entries.map((kv) { final volume = kv.key?.path; @@ -117,26 +105,22 @@ class Favourites with ChangeNotifier { @immutable class FavouriteRow extends Equatable { - final int contentId; - final String path; + final int entryId; @override - List get props => [contentId, path]; + List get props => [entryId]; const FavouriteRow({ - required this.contentId, - required this.path, + required this.entryId, }); factory FavouriteRow.fromMap(Map map) { return FavouriteRow( - contentId: map['contentId'] ?? 0, - path: map['path'] ?? '', + entryId: map['id'] as int, ); } Map toMap() => { - 'contentId': contentId, - 'path': path, + 'id': entryId, }; } diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 9edf03541..48fb78ab5 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -1,6 +1,7 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; @@ -57,21 +58,36 @@ class AlbumFilter extends CollectionFilter { Future color(BuildContext context) { // do not use async/await and rely on `SynchronousFuture` // to prevent rebuilding of the `FutureBuilder` listening on this future - if (androidFileUtils.getAlbumType(album) == AlbumType.app) { - if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!); + final albumType = androidFileUtils.getAlbumType(album); + switch (albumType) { + case AlbumType.regular: + break; + case AlbumType.app: + if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!); - final packageName = androidFileUtils.getAlbumAppPackageName(album); - if (packageName != null) { - return PaletteGenerator.fromImageProvider( - AppIconImage(packageName: packageName, size: 24), - ).then((palette) async { - // `dominantColor` is most representative but can have low contrast with a dark background - // `vibrantColor` is usually representative and has good contrast with a dark background - final color = palette.vibrantColor?.color ?? (await super.color(context)); - _appColors[album] = color; - return color; - }); - } + final packageName = androidFileUtils.getAlbumAppPackageName(album); + if (packageName != null) { + return PaletteGenerator.fromImageProvider( + AppIconImage(packageName: packageName, size: 24), + ).then((palette) async { + // `dominantColor` is most representative but can have low contrast with a dark background + // `vibrantColor` is usually representative and has good contrast with a dark background + final color = palette.vibrantColor?.color ?? (await super.color(context)); + _appColors[album] = color; + return color; + }); + } + break; + case AlbumType.camera: + return SynchronousFuture(AColors.albumCamera); + case AlbumType.download: + return SynchronousFuture(AColors.albumDownload); + case AlbumType.screenRecordings: + return SynchronousFuture(AColors.albumScreenRecordings); + case AlbumType.screenshots: + return SynchronousFuture(AColors.albumScreenshots); + case AlbumType.videoCaptures: + return SynchronousFuture(AColors.albumVideoCaptures); } return super.color(context); } diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index e211b625b..90664b8c0 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -1,7 +1,7 @@ import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/settings/coordinate_format.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/coordinate_format.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/geo_utils.dart'; diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index 9805fdfc2..afa6116a6 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -1,4 +1,5 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; @@ -32,7 +33,7 @@ class FavouriteFilter extends CollectionFilter { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size); @override - Future color(BuildContext context) => SynchronousFuture(Colors.red); + Future color(BuildContext context) => SynchronousFuture(AColors.favourite); @override String get category => type; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index 058715629..2c9da7615 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -10,6 +10,7 @@ import 'package:aves/model/filters/path.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/tag.dart'; +import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/type.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:collection/collection.dart'; @@ -20,6 +21,7 @@ import 'package:flutter/widgets.dart'; @immutable abstract class CollectionFilter extends Equatable implements Comparable { static const List categoryOrder = [ + TrashFilter.type, QueryFilter.type, MimeFilter.type, AlbumFilter.type, @@ -64,6 +66,8 @@ abstract class CollectionFilter extends Equatable implements Comparable entry.mimeType == lowMime; _label = MimeUtils.displayType(lowMime); } _icon = icon ?? AIcons.vector; + _color = color ?? stringToColor(_label); } MimeFilter.fromMap(Map json) @@ -70,6 +78,9 @@ class MimeFilter extends CollectionFilter { @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); + @override + Future color(BuildContext context) => SynchronousFuture(_color); + @override String get category => type; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index aae48e395..b7c5e210d 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -22,7 +22,7 @@ class QueryFilter extends CollectionFilter { var upQuery = query.toUpperCase(); if (upQuery.startsWith('ID:')) { final id = int.tryParse(upQuery.substring(3)); - _test = (entry) => entry.contentId == id; + _test = (entry) => entry.id == id; return; } diff --git a/lib/model/filters/trash.dart b/lib/model/filters/trash.dart new file mode 100644 index 000000000..a8f902c85 --- /dev/null +++ b/lib/model/filters/trash.dart @@ -0,0 +1,38 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:flutter/material.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; + +class TrashFilter extends CollectionFilter { + static const type = 'trash'; + + static const instance = TrashFilter._private(); + + @override + List get props => []; + + const TrashFilter._private(); + + @override + Map toMap() => { + 'type': type, + }; + + @override + EntryFilter get test => (entry) => entry.trashed; + + @override + String get universalLabel => type; + + @override + String getLabel(BuildContext context) => context.l10n.filterBinLabel; + + @override + Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.bin, size: size); + + @override + String get category => type; + + @override + String get key => type; +} diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index efc6420a2..1d2b986f0 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -1,6 +1,8 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class TypeFilter extends CollectionFilter { @@ -16,6 +18,7 @@ class TypeFilter extends CollectionFilter { final String itemType; late final EntryFilter _test; late final IconData _icon; + late final Color _color; static final animated = TypeFilter._private(_animated); static final geotiff = TypeFilter._private(_geotiff); @@ -32,26 +35,32 @@ class TypeFilter extends CollectionFilter { case _animated: _test = (entry) => entry.isAnimated; _icon = AIcons.animated; + _color = AColors.animated; break; case _geotiff: _test = (entry) => entry.isGeotiff; _icon = AIcons.geo; + _color = AColors.geotiff; break; case _motionPhoto: _test = (entry) => entry.isMotionPhoto; _icon = AIcons.motionPhoto; + _color = AColors.motionPhoto; break; case _panorama: _test = (entry) => entry.isImage && entry.is360; _icon = AIcons.threeSixty; + _color = AColors.panorama; break; case _raw: _test = (entry) => entry.isRaw; _icon = AIcons.raw; + _color = AColors.raw; break; case _sphericalVideo: _test = (entry) => entry.isVideo && entry.is360; _icon = AIcons.threeSixty; + _color = AColors.sphericalVideo; break; } } @@ -96,6 +105,9 @@ class TypeFilter extends CollectionFilter { @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); + @override + Future color(BuildContext context) => SynchronousFuture(_color); + @override String get category => type; diff --git a/lib/model/metadata/address.dart b/lib/model/metadata/address.dart index d7e5f232e..b05ecd988 100644 --- a/lib/model/metadata/address.dart +++ b/lib/model/metadata/address.dart @@ -4,16 +4,16 @@ import 'package:flutter/widgets.dart'; @immutable class AddressDetails extends Equatable { - final int? contentId; + final int id; final String? countryCode, countryName, adminArea, locality; String? get place => locality != null && locality!.isNotEmpty ? locality : adminArea; @override - List get props => [contentId, countryCode, countryName, adminArea, locality]; + List get props => [id, countryCode, countryName, adminArea, locality]; const AddressDetails({ - this.contentId, + required this.id, this.countryCode, this.countryName, this.adminArea, @@ -21,10 +21,10 @@ class AddressDetails extends Equatable { }); AddressDetails copyWith({ - int? contentId, + int? id, }) { return AddressDetails( - contentId: contentId ?? this.contentId, + id: id ?? this.id, countryCode: countryCode, countryName: countryName, adminArea: adminArea, @@ -34,7 +34,7 @@ class AddressDetails extends Equatable { factory AddressDetails.fromMap(Map map) { return AddressDetails( - contentId: map['contentId'] as int?, + id: map['id'] as int, countryCode: map['countryCode'] as String?, countryName: map['countryName'] as String?, adminArea: map['adminArea'] as String?, @@ -43,7 +43,7 @@ class AddressDetails extends Equatable { } Map toMap() => { - 'contentId': contentId, + 'id': id, 'countryCode': countryCode, 'countryName': countryName, 'adminArea': adminArea, diff --git a/lib/model/metadata/catalog.dart b/lib/model/metadata/catalog.dart index 008065451..2c380018f 100644 --- a/lib/model/metadata/catalog.dart +++ b/lib/model/metadata/catalog.dart @@ -2,7 +2,8 @@ import 'package:aves/services/geocoding_service.dart'; import 'package:flutter/foundation.dart'; class CatalogMetadata { - final int? contentId, dateMillis; + final int id; + final int? dateMillis; final bool isAnimated, isGeotiff, is360, isMultiPage; bool isFlipped; int? rotationDegrees; @@ -19,7 +20,7 @@ class CatalogMetadata { static const _isMultiPageMask = 1 << 4; CatalogMetadata({ - this.contentId, + required this.id, this.mimeType, this.dateMillis, this.isAnimated = false, @@ -49,14 +50,14 @@ class CatalogMetadata { } CatalogMetadata copyWith({ - int? contentId, + int? id, String? mimeType, int? dateMillis, bool? isMultiPage, int? rotationDegrees, }) { return CatalogMetadata( - contentId: contentId ?? this.contentId, + id: id ?? this.id, mimeType: mimeType ?? this.mimeType, dateMillis: dateMillis ?? this.dateMillis, isAnimated: isAnimated, @@ -76,7 +77,7 @@ class CatalogMetadata { factory CatalogMetadata.fromMap(Map map) { final flags = map['flags'] ?? 0; return CatalogMetadata( - contentId: map['contentId'], + id: map['id'], mimeType: map['mimeType'], dateMillis: map['dateMillis'] ?? 0, isAnimated: flags & _isAnimatedMask != 0, @@ -95,7 +96,7 @@ class CatalogMetadata { } Map toMap() => { - 'contentId': contentId, + 'id': id, 'mimeType': mimeType, 'dateMillis': dateMillis, 'flags': (isAnimated ? _isAnimatedMask : 0) | (isFlipped ? _isFlippedMask : 0) | (isGeotiff ? _isGeotiffMask : 0) | (is360 ? _is360Mask : 0) | (isMultiPage ? _isMultiPageMask : 0), @@ -108,5 +109,5 @@ class CatalogMetadata { }; @override - String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, is360=$is360, isMultiPage=$isMultiPage, rotationDegrees=$rotationDegrees, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription, latitude=$latitude, longitude=$longitude, rating=$rating}'; + String toString() => '$runtimeType#${shortHash(this)}{id=$id, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, is360=$is360, isMultiPage=$isMultiPage, rotationDegrees=$rotationDegrees, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription, latitude=$latitude, longitude=$longitude, rating=$rating}'; } diff --git a/lib/model/metadata/trash.dart b/lib/model/metadata/trash.dart new file mode 100644 index 000000000..260661d29 --- /dev/null +++ b/lib/model/metadata/trash.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; + +@immutable +class TrashDetails extends Equatable { + final int id; + final String path; + final int dateMillis; + + @override + List get props => [id, path, dateMillis]; + + const TrashDetails({ + required this.id, + required this.path, + required this.dateMillis, + }); + + TrashDetails copyWith({ + int? id, + }) { + return TrashDetails( + id: id ?? this.id, + path: path, + dateMillis: dateMillis, + ); + } + + factory TrashDetails.fromMap(Map map) { + return TrashDetails( + id: map['id'] as int, + path: map['path'] as String, + dateMillis: map['dateMillis'] as int, + ); + } + + Map toMap() => { + 'id': id, + 'path': path, + 'dateMillis': dateMillis, + }; +} diff --git a/lib/model/metadata_db_upgrade.dart b/lib/model/metadata_db_upgrade.dart deleted file mode 100644 index d69e7857c..000000000 --- a/lib/model/metadata_db_upgrade.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:aves/model/metadata_db.dart'; -import 'package:flutter/foundation.dart'; -import 'package:sqflite/sqflite.dart'; - -class MetadataDbUpgrader { - static const entryTable = SqfliteMetadataDb.entryTable; - static const metadataTable = SqfliteMetadataDb.metadataTable; - static const coverTable = SqfliteMetadataDb.coverTable; - static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable; - - // warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported - // on SQLite <3.25.0, bundled on older Android devices - static Future upgradeDb(Database db, int oldVersion, int newVersion) async { - while (oldVersion < newVersion) { - switch (oldVersion) { - case 1: - await _upgradeFrom1(db); - break; - case 2: - await _upgradeFrom2(db); - break; - case 3: - await _upgradeFrom3(db); - break; - case 4: - await _upgradeFrom4(db); - break; - case 5: - await _upgradeFrom5(db); - break; - } - oldVersion++; - } - } - - static Future _upgradeFrom1(Database db) async { - debugPrint('upgrading DB from v1'); - // rename column 'orientationDegrees' to 'sourceRotationDegrees' - await db.transaction((txn) async { - const newEntryTable = '${entryTable}TEMP'; - await db.execute('CREATE TABLE $newEntryTable(' - 'contentId INTEGER PRIMARY KEY' - ', uri TEXT' - ', path TEXT' - ', sourceMimeType TEXT' - ', width INTEGER' - ', height INTEGER' - ', sourceRotationDegrees INTEGER' - ', sizeBytes INTEGER' - ', title TEXT' - ', dateModifiedSecs INTEGER' - ', sourceDateTakenMillis INTEGER' - ', durationMillis INTEGER' - ')'); - await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)' - ' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis' - ' FROM $entryTable;'); - await db.execute('DROP TABLE $entryTable;'); - await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;'); - }); - - // rename column 'videoRotation' to 'rotationDegrees' - await db.transaction((txn) async { - const newMetadataTable = '${metadataTable}TEMP'; - await db.execute('CREATE TABLE $newMetadataTable(' - 'contentId INTEGER PRIMARY KEY' - ', mimeType TEXT' - ', dateMillis INTEGER' - ', isAnimated INTEGER' - ', rotationDegrees INTEGER' - ', xmpSubjects TEXT' - ', xmpTitleDescription TEXT' - ', latitude REAL' - ', longitude REAL' - ')'); - await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,isAnimated,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)' - ' SELECT contentId,mimeType,dateMillis,isAnimated,videoRotation,xmpSubjects,xmpTitleDescription,latitude,longitude' - ' FROM $metadataTable;'); - await db.rawInsert('UPDATE $newMetadataTable SET rotationDegrees = NULL WHERE rotationDegrees = 0;'); - await db.execute('DROP TABLE $metadataTable;'); - await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); - }); - - // new column 'isFlipped' - await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;'); - } - - static Future _upgradeFrom2(Database db) async { - debugPrint('upgrading DB from v2'); - // merge columns 'isAnimated' and 'isFlipped' into 'flags' - await db.transaction((txn) async { - const newMetadataTable = '${metadataTable}TEMP'; - await db.execute('CREATE TABLE $newMetadataTable(' - 'contentId INTEGER PRIMARY KEY' - ', mimeType TEXT' - ', dateMillis INTEGER' - ', flags INTEGER' - ', rotationDegrees INTEGER' - ', xmpSubjects TEXT' - ', xmpTitleDescription TEXT' - ', latitude REAL' - ', longitude REAL' - ')'); - await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)' - ' SELECT contentId,mimeType,dateMillis,ifnull(isAnimated,0)+ifnull(isFlipped,0)*2,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude' - ' FROM $metadataTable;'); - await db.execute('DROP TABLE $metadataTable;'); - await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); - }); - } - - static Future _upgradeFrom3(Database db) async { - debugPrint('upgrading DB from v3'); - await db.execute('CREATE TABLE $coverTable(' - 'filter TEXT PRIMARY KEY' - ', contentId INTEGER' - ')'); - } - - static Future _upgradeFrom4(Database db) async { - debugPrint('upgrading DB from v4'); - await db.execute('CREATE TABLE $videoPlaybackTable(' - 'contentId INTEGER PRIMARY KEY' - ', resumeTimeMillis INTEGER' - ')'); - } - - static Future _upgradeFrom5(Database db) async { - debugPrint('upgrading DB from v5'); - await db.execute('ALTER TABLE $metadataTable ADD COLUMN rating INTEGER;'); - } -} diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index 7593a4c0b..6e3ad53d8 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -87,7 +87,11 @@ class MultiPageInfo { // and retrieve cached images for it final pageId = eraseDefaultPageId && pageInfo.isDefault ? null : pageInfo.pageId; + // dynamically extracted video is not in the trash like the original motion photo + final trashed = (mainEntry.isMotionPhoto && pageInfo.isVideo) ? false : mainEntry.trashed; + return AvesEntry( + id: mainEntry.id, uri: pageInfo.uri ?? mainEntry.uri, path: mainEntry.path, contentId: mainEntry.contentId, @@ -101,13 +105,15 @@ class MultiPageInfo { dateModifiedSecs: mainEntry.dateModifiedSecs, sourceDateTakenMillis: mainEntry.sourceDateTakenMillis, durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis, + trashed: trashed, ) ..catalogMetadata = mainEntry.catalogMetadata?.copyWith( mimeType: pageInfo.mimeType, isMultiPage: false, rotationDegrees: pageInfo.rotationDegrees, ) - ..addressDetails = mainEntry.addressDetails?.copyWith(); + ..addressDetails = mainEntry.addressDetails?.copyWith() + ..trashDetails = trashed ? mainEntry.trashDetails : null; } @override diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 9f7a6f968..e3c2de204 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -3,7 +3,7 @@ import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/video_actions.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; @@ -16,12 +16,13 @@ class SettingsDefaults { static const canUseAnalysisService = true; static const isInstalledAppAccessAllowed = false; static const isErrorReportingAllowed = false; + static const tileLayout = TileLayout.grid; + + // navigation static const mustBackTwiceToExit = true; static const keepScreenOn = KeepScreenOn.viewerOnly; static const homePage = HomePageSetting.collection; - static const tileLayout = TileLayout.grid; - - // drawer + static const confirmationDialogs = ConfirmationDialog.values; static final drawerTypeBookmarks = [ null, MimeFilter.video, @@ -100,10 +101,17 @@ class SettingsDefaults { // search static const saveSearchHistory = true; + // bin + static const enableBin = true; + // accessibility static const accessibilityAnimations = AccessibilityAnimations.system; static const timeToTakeAction = AccessibilityTimeout.appDefault; // `timeToTakeAction` has a contextual default value // file picker static const filePickerShowHiddenFiles = false; + + // platform settings + static const isRotationLocked = false; + static const areAnimationsRemoved = false; } diff --git a/lib/model/settings/accessibility_animations.dart b/lib/model/settings/enums/accessibility_animations.dart similarity index 100% rename from lib/model/settings/accessibility_animations.dart rename to lib/model/settings/enums/accessibility_animations.dart diff --git a/lib/model/settings/accessibility_timeout.dart b/lib/model/settings/enums/accessibility_timeout.dart similarity index 100% rename from lib/model/settings/accessibility_timeout.dart rename to lib/model/settings/enums/accessibility_timeout.dart diff --git a/lib/model/settings/enums/confirmation_dialogs.dart b/lib/model/settings/enums/confirmation_dialogs.dart new file mode 100644 index 000000000..4f1a58742 --- /dev/null +++ b/lib/model/settings/enums/confirmation_dialogs.dart @@ -0,0 +1,15 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +import 'enums.dart'; + +extension ExtraConfirmationDialog on ConfirmationDialog { + String getName(BuildContext context) { + switch (this) { + case ConfirmationDialog.delete: + return context.l10n.settingsConfirmationDialogDeleteItems; + case ConfirmationDialog.moveToBin: + return context.l10n.settingsConfirmationDialogMoveToBinItems; + } + } +} diff --git a/lib/model/settings/coordinate_format.dart b/lib/model/settings/enums/coordinate_format.dart similarity index 100% rename from lib/model/settings/coordinate_format.dart rename to lib/model/settings/enums/coordinate_format.dart diff --git a/lib/model/settings/entry_background.dart b/lib/model/settings/enums/entry_background.dart similarity index 100% rename from lib/model/settings/entry_background.dart rename to lib/model/settings/enums/entry_background.dart diff --git a/lib/model/settings/enums.dart b/lib/model/settings/enums/enums.dart similarity index 92% rename from lib/model/settings/enums.dart rename to lib/model/settings/enums/enums.dart index f4678d208..8957d0141 100644 --- a/lib/model/settings/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -1,16 +1,18 @@ -enum CoordinateFormat { dms, decimal } - enum AccessibilityAnimations { system, disabled, enabled } enum AccessibilityTimeout { system, appDefault, s10, s30, s60, s120 } -enum EntryBackground { black, white, checkered } +enum ConfirmationDialog { delete, moveToBin } -enum HomePageSetting { collection, albums } +enum CoordinateFormat { dms, decimal } + +enum EntryBackground { black, white, checkered } // browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } +enum HomePageSetting { collection, albums } + enum KeepScreenOn { never, viewerOnly, always } enum UnitSystem { metric, imperial } diff --git a/lib/model/settings/home_page.dart b/lib/model/settings/enums/home_page.dart similarity index 100% rename from lib/model/settings/home_page.dart rename to lib/model/settings/enums/home_page.dart diff --git a/lib/model/settings/map_style.dart b/lib/model/settings/enums/map_style.dart similarity index 100% rename from lib/model/settings/map_style.dart rename to lib/model/settings/enums/map_style.dart diff --git a/lib/model/settings/screen_on.dart b/lib/model/settings/enums/screen_on.dart similarity index 100% rename from lib/model/settings/screen_on.dart rename to lib/model/settings/enums/screen_on.dart diff --git a/lib/model/settings/unit_system.dart b/lib/model/settings/enums/unit_system.dart similarity index 100% rename from lib/model/settings/unit_system.dart rename to lib/model/settings/enums/unit_system.dart diff --git a/lib/model/settings/video_loop_mode.dart b/lib/model/settings/enums/video_loop_mode.dart similarity index 100% rename from lib/model/settings/video_loop_mode.dart rename to lib/model/settings/enums/video_loop_mode.dart diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 2740db8fd..4d32bff5a 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -7,25 +7,22 @@ import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/video_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/defaults.dart'; -import 'package:aves/model/settings/enums.dart'; -import 'package:aves/model/settings/map_style.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:shared_preferences/shared_preferences.dart'; final Settings settings = Settings._private(); class Settings extends ChangeNotifier { final EventChannel _platformSettingsChangeChannel = const EventChannel('deckers.thibault/aves/settings_change'); - final StreamController _updateStreamController = StreamController.broadcast(); + final StreamController _updateStreamController = StreamController.broadcast(); - Stream get updateStream => _updateStreamController.stream; - - static SharedPreferences? _prefs; + Stream get updateStream => _updateStreamController.stream; Settings._private(); @@ -34,6 +31,9 @@ class Settings extends ChangeNotifier { catalogTimeZoneKey, videoShowRawTimedTextKey, searchHistoryKey, + platformAccelerometerRotationKey, + platformTransitionAnimationScaleKey, + topEntryIdsKey, }; // app @@ -42,14 +42,16 @@ class Settings extends ChangeNotifier { static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed'; static const isErrorReportingAllowedKey = 'is_crashlytics_enabled'; static const localeKey = 'locale'; - static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; - static const keepScreenOnKey = 'keep_screen_on'; - static const homePageKey = 'home_page'; static const catalogTimeZoneKey = 'catalog_time_zone'; static const tileExtentPrefixKey = 'tile_extent_'; static const tileLayoutPrefixKey = 'tile_layout_'; + static const topEntryIdsKey = 'top_entry_ids'; - // drawer + // navigation + static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; + static const keepScreenOnKey = 'keep_screen_on'; + static const homePageKey = 'home_page'; + static const confirmationDialogsKey = 'confirmation_dialogs'; static const drawerTypeBookmarksKey = 'drawer_type_bookmarks'; static const drawerAlbumBookmarksKey = 'drawer_album_bookmarks'; static const drawerPageBookmarksKey = 'drawer_page_bookmarks'; @@ -110,6 +112,9 @@ class Settings extends ChangeNotifier { static const saveSearchHistoryKey = 'save_search_history'; static const searchHistoryKey = 'search_history'; + // bin + static const enableBinKey = 'enable_bin'; + // accessibility static const accessibilityAnimationsKey = 'accessibility_animations'; static const timeToTakeActionKey = 'time_to_take_action'; @@ -124,16 +129,10 @@ class Settings extends ChangeNotifier { // cf Android `Settings.Global.TRANSITION_ANIMATION_SCALE` static const platformTransitionAnimationScaleKey = 'transition_animation_scale'; - bool get initialized => _prefs != null; + bool get initialized => settingsStore.initialized; - Future init({ - required bool monitorPlatformSettings, - bool isRotationLocked = false, - bool areAnimationsRemoved = false, - }) async { - _prefs = await SharedPreferences.getInstance(); - _isRotationLocked = isRotationLocked; - _areAnimationsRemoved = areAnimationsRemoved; + Future init({required bool monitorPlatformSettings}) async { + await settingsStore.init(); if (monitorPlatformSettings) { _platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChange(event as Map?)); } @@ -141,9 +140,9 @@ class Settings extends ChangeNotifier { Future reset({required bool includeInternalKeys}) async { if (includeInternalKeys) { - await _prefs!.clear(); + await settingsStore.clear(); } else { - await Future.forEach(_prefs!.getKeys().whereNot(internalKeys.contains), _prefs!.remove); + await Future.forEach(settingsStore.getKeys().whereNot(Settings.internalKeys.contains), settingsStore.remove); } } @@ -189,7 +188,7 @@ class Settings extends ChangeNotifier { Locale? get locale { // exceptionally allow getting locale before settings are initialized - final tag = _prefs?.getString(localeKey); + final tag = initialized ? getString(localeKey) : null; if (tag != null) { final codes = tag.split(localeSeparator); return Locale.fromSubtags( @@ -238,6 +237,24 @@ class Settings extends ChangeNotifier { return _appliedLocale!; } + String get catalogTimeZone => getString(catalogTimeZoneKey) ?? ''; + + set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); + + double getTileExtent(String routeName) => getDouble(tileExtentPrefixKey + routeName) ?? 0; + + void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue); + + TileLayout getTileLayout(String routeName) => getEnumOrDefault(tileLayoutPrefixKey + routeName, SettingsDefaults.tileLayout, TileLayout.values); + + void setTileLayout(String routeName, TileLayout newValue) => setAndNotify(tileLayoutPrefixKey + routeName, newValue.toString()); + + List? get topEntryIds => getStringList(topEntryIdsKey)?.map(int.tryParse).whereNotNull().toList(); + + set topEntryIds(List? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList()); + + // navigation + bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, SettingsDefaults.mustBackTwiceToExit); set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue); @@ -250,22 +267,12 @@ class Settings extends ChangeNotifier { set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); - String get catalogTimeZone => _prefs!.getString(catalogTimeZoneKey) ?? ''; + List get confirmationDialogs => getEnumListOrDefault(confirmationDialogsKey, SettingsDefaults.confirmationDialogs, ConfirmationDialog.values); - set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); - - double getTileExtent(String routeName) => _prefs!.getDouble(tileExtentPrefixKey + routeName) ?? 0; - - void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue); - - TileLayout getTileLayout(String routeName) => getEnumOrDefault(tileLayoutPrefixKey + routeName, SettingsDefaults.tileLayout, TileLayout.values); - - void setTileLayout(String routeName, TileLayout newValue) => setAndNotify(tileLayoutPrefixKey + routeName, newValue.toString()); - - // drawer + set confirmationDialogs(List newValue) => setAndNotify(confirmationDialogsKey, newValue.map((v) => v.toString()).toList()); List get drawerTypeBookmarks => - (_prefs!.getStringList(drawerTypeBookmarksKey))?.map((v) { + (getStringList(drawerTypeBookmarksKey))?.map((v) { if (v.isEmpty) return null; return CollectionFilter.fromJson(v); }).toList() ?? @@ -273,11 +280,11 @@ class Settings extends ChangeNotifier { set drawerTypeBookmarks(List newValue) => setAndNotify(drawerTypeBookmarksKey, newValue.map((filter) => filter?.toJson() ?? '').toList()); - List? get drawerAlbumBookmarks => _prefs!.getStringList(drawerAlbumBookmarksKey); + List? get drawerAlbumBookmarks => getStringList(drawerAlbumBookmarksKey); set drawerAlbumBookmarks(List? newValue) => setAndNotify(drawerAlbumBookmarksKey, newValue); - List get drawerPageBookmarks => _prefs!.getStringList(drawerPageBookmarksKey) ?? SettingsDefaults.drawerPageBookmarks; + List get drawerPageBookmarks => getStringList(drawerPageBookmarksKey) ?? SettingsDefaults.drawerPageBookmarks; set drawerPageBookmarks(List newValue) => setAndNotify(drawerPageBookmarksKey, newValue); @@ -341,14 +348,25 @@ class Settings extends ChangeNotifier { set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); - Set get pinnedFilters => (_prefs!.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); + Set get pinnedFilters => (getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); set pinnedFilters(Set newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); - Set get hiddenFilters => (_prefs!.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); + Set get hiddenFilters => (getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); set hiddenFilters(Set newValue) => setAndNotify(hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList()); + void changeFilterVisibility(Set filters, bool visible) { + final _hiddenFilters = hiddenFilters; + if (visible) { + _hiddenFilters.removeAll(filters); + } else { + _hiddenFilters.addAll(filters); + searchHistory = searchHistory..removeWhere(filters.contains); + } + hiddenFilters = _hiddenFilters; + } + // viewer List get viewerQuickActions => getEnumListOrDefault(viewerQuickActionsKey, SettingsDefaults.viewerQuickActions, EntryAction.values); @@ -415,7 +433,7 @@ class Settings extends ChangeNotifier { // subtitles - double get subtitleFontSize => _prefs!.getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize; + double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize; set subtitleFontSize(double newValue) => setAndNotify(subtitleFontSizeKey, newValue); @@ -427,11 +445,11 @@ class Settings extends ChangeNotifier { set subtitleShowOutline(bool newValue) => setAndNotify(subtitleShowOutlineKey, newValue); - Color get subtitleTextColor => Color(_prefs!.getInt(subtitleTextColorKey) ?? SettingsDefaults.subtitleTextColor.value); + Color get subtitleTextColor => Color(getInt(subtitleTextColorKey) ?? SettingsDefaults.subtitleTextColor.value); set subtitleTextColor(Color newValue) => setAndNotify(subtitleTextColorKey, newValue.value); - Color get subtitleBackgroundColor => Color(_prefs!.getInt(subtitleBackgroundColorKey) ?? SettingsDefaults.subtitleBackgroundColor.value); + Color get subtitleBackgroundColor => Color(getInt(subtitleBackgroundColorKey) ?? SettingsDefaults.subtitleBackgroundColor.value); set subtitleBackgroundColor(Color newValue) => setAndNotify(subtitleBackgroundColorKey, newValue.value); @@ -441,7 +459,7 @@ class Settings extends ChangeNotifier { set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); - double get infoMapZoom => _prefs!.getDouble(infoMapZoomKey) ?? SettingsDefaults.infoMapZoom; + double get infoMapZoom => getDouble(infoMapZoomKey) ?? SettingsDefaults.infoMapZoom; set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); @@ -459,10 +477,16 @@ class Settings extends ChangeNotifier { set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue); - List get searchHistory => (_prefs!.getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList(); + List get searchHistory => (getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList(); set searchHistory(List newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); + // bin + + bool get enableBin => getBoolOrDefault(enableBinKey, SettingsDefaults.enableBin); + + set enableBin(bool newValue) => setAndNotify(enableBinKey, newValue); + // accessibility AccessibilityAnimations get accessibilityAnimations => getEnumOrDefault(accessibilityAnimationsKey, SettingsDefaults.accessibilityAnimations, AccessibilityAnimations.values); @@ -481,11 +505,19 @@ class Settings extends ChangeNotifier { // convenience methods + int? getInt(String key) => settingsStore.getInt(key); + + double? getDouble(String key) => settingsStore.getDouble(key); + + String? getString(String key) => settingsStore.getString(key); + + List? getStringList(String key) => settingsStore.getStringList(key); + // ignore: avoid_positional_boolean_parameters - bool getBoolOrDefault(String key, bool defaultValue) => _prefs!.getBool(key) ?? defaultValue; + bool getBoolOrDefault(String key, bool defaultValue) => settingsStore.getBool(key) ?? defaultValue; T getEnumOrDefault(String key, T defaultValue, Iterable values) { - final valueString = _prefs!.getString(key); + final valueString = settingsStore.getString(key); for (final v in values) { if (v.toString() == valueString) { return v; @@ -495,31 +527,31 @@ class Settings extends ChangeNotifier { } List getEnumListOrDefault(String key, List defaultValue, Iterable values) { - return _prefs!.getStringList(key)?.map((s) => values.firstWhereOrNull((v) => v.toString() == s)).whereNotNull().toList() ?? defaultValue; + return settingsStore.getStringList(key)?.map((s) => values.firstWhereOrNull((v) => v.toString() == s)).whereNotNull().toList() ?? defaultValue; } void setAndNotify(String key, dynamic newValue) { - var oldValue = _prefs!.get(key); + var oldValue = settingsStore.get(key); if (newValue == null) { - _prefs!.remove(key); + settingsStore.remove(key); } else if (newValue is String) { - oldValue = _prefs!.getString(key); - _prefs!.setString(key, newValue); + oldValue = settingsStore.getString(key); + settingsStore.setString(key, newValue); } else if (newValue is List) { - oldValue = _prefs!.getStringList(key); - _prefs!.setStringList(key, newValue); + oldValue = settingsStore.getStringList(key); + settingsStore.setStringList(key, newValue); } else if (newValue is int) { - oldValue = _prefs!.getInt(key); - _prefs!.setInt(key, newValue); + oldValue = settingsStore.getInt(key); + settingsStore.setInt(key, newValue); } else if (newValue is double) { - oldValue = _prefs!.getDouble(key); - _prefs!.setDouble(key, newValue); + oldValue = settingsStore.getDouble(key); + settingsStore.setDouble(key, newValue); } else if (newValue is bool) { - oldValue = _prefs!.getBool(key); - _prefs!.setBool(key, newValue); + oldValue = settingsStore.getBool(key); + settingsStore.setBool(key, newValue); } if (oldValue != newValue) { - _updateStreamController.add(key); + _updateStreamController.add(SettingsChangedEvent(key, oldValue, newValue)); notifyListeners(); } } @@ -527,50 +559,33 @@ class Settings extends ChangeNotifier { // platform settings void _onPlatformSettingsChange(Map? fields) { - var changed = false; fields?.forEach((key, value) { switch (key) { case platformAccelerometerRotationKey: if (value is num) { - final newValue = value == 0; - if (_isRotationLocked != newValue) { - _isRotationLocked = newValue; - if (!_isRotationLocked) { - windowService.requestOrientation(); - } - _updateStreamController.add(key); - changed = true; - } + isRotationLocked = value == 0; } break; case platformTransitionAnimationScaleKey: if (value is num) { - final newValue = value == 0; - if (_areAnimationsRemoved != newValue) { - _areAnimationsRemoved = newValue; - _updateStreamController.add(key); - changed = true; - } + areAnimationsRemoved = value == 0; } } }); - if (changed) { - notifyListeners(); - } } - bool _isRotationLocked = false; + bool get isRotationLocked => getBoolOrDefault(platformAccelerometerRotationKey, SettingsDefaults.isRotationLocked); - bool get isRotationLocked => _isRotationLocked; + set isRotationLocked(bool newValue) => setAndNotify(platformAccelerometerRotationKey, newValue); - bool _areAnimationsRemoved = false; + bool get areAnimationsRemoved => getBoolOrDefault(platformTransitionAnimationScaleKey, SettingsDefaults.areAnimationsRemoved); - bool get areAnimationsRemoved => _areAnimationsRemoved; + set areAnimationsRemoved(bool newValue) => setAndNotify(platformTransitionAnimationScaleKey, newValue); // import/export Map export() => Map.fromEntries( - _prefs!.getKeys().whereNot(internalKeys.contains).map((k) => MapEntry(k, _prefs!.get(k))), + settingsStore.getKeys().whereNot(internalKeys.contains).map((k) => MapEntry(k, settingsStore.get(k))), ); Future import(dynamic jsonMap) async { @@ -579,37 +594,39 @@ class Settings extends ChangeNotifier { await reset(includeInternalKeys: false); // apply user modifications - jsonMap.forEach((key, value) { - if (value == null) { - _prefs!.remove(key); + jsonMap.forEach((key, newValue) { + final oldValue = settingsStore.get(key); + + if (newValue == null) { + settingsStore.remove(key); } else if (key.startsWith(tileExtentPrefixKey)) { - if (value is double) { - _prefs!.setDouble(key, value); + if (newValue is double) { + settingsStore.setDouble(key, newValue); } else { - debugPrint('failed to import key=$key, value=$value is not a double'); + debugPrint('failed to import key=$key, value=$newValue is not a double'); } } else if (key.startsWith(tileLayoutPrefixKey)) { - if (value is String) { - _prefs!.setString(key, value); + if (newValue is String) { + settingsStore.setString(key, newValue); } else { - debugPrint('failed to import key=$key, value=$value is not a string'); + debugPrint('failed to import key=$key, value=$newValue is not a string'); } } else { switch (key) { case subtitleTextColorKey: case subtitleBackgroundColorKey: - if (value is int) { - _prefs!.setInt(key, value); + if (newValue is int) { + settingsStore.setInt(key, newValue); } else { - debugPrint('failed to import key=$key, value=$value is not an int'); + debugPrint('failed to import key=$key, value=$newValue is not an int'); } break; case subtitleFontSizeKey: case infoMapZoomKey: - if (value is double) { - _prefs!.setDouble(key, value); + if (newValue is double) { + settingsStore.setDouble(key, newValue); } else { - debugPrint('failed to import key=$key, value=$value is not a double'); + debugPrint('failed to import key=$key, value=$newValue is not a double'); } break; case isInstalledAppAccessAllowedKey: @@ -634,10 +651,10 @@ class Settings extends ChangeNotifier { case subtitleShowOutlineKey: case saveSearchHistoryKey: case filePickerShowHiddenFilesKey: - if (value is bool) { - _prefs!.setBool(key, value); + if (newValue is bool) { + settingsStore.setBool(key, newValue); } else { - debugPrint('failed to import key=$key, value=$value is not a bool'); + debugPrint('failed to import key=$key, value=$newValue is not a bool'); } break; case localeKey: @@ -657,12 +674,13 @@ class Settings extends ChangeNotifier { case unitSystemKey: case accessibilityAnimationsKey: case timeToTakeActionKey: - if (value is String) { - _prefs!.setString(key, value); + if (newValue is String) { + settingsStore.setString(key, newValue); } else { - debugPrint('failed to import key=$key, value=$value is not a string'); + debugPrint('failed to import key=$key, value=$newValue is not a string'); } break; + case confirmationDialogsKey: case drawerTypeBookmarksKey: case drawerAlbumBookmarksKey: case drawerPageBookmarksKey: @@ -672,17 +690,29 @@ class Settings extends ChangeNotifier { case collectionSelectionQuickActionsKey: case viewerQuickActionsKey: case videoQuickActionsKey: - if (value is List) { - _prefs!.setStringList(key, value.cast()); + if (newValue is List) { + settingsStore.setStringList(key, newValue.cast()); } else { - debugPrint('failed to import key=$key, value=$value is not a list'); + debugPrint('failed to import key=$key, value=$newValue is not a list'); } break; } } - _updateStreamController.add(key); + if (oldValue != newValue) { + _updateStreamController.add(SettingsChangedEvent(key, oldValue, newValue)); + } }); notifyListeners(); } } } + +@immutable +class SettingsChangedEvent { + final String key; + final dynamic oldValue; + final dynamic newValue; + + // old and new values as stored, e.g. `List` for collections + const SettingsChangedEvent(this.key, this.oldValue, this.newValue); +} diff --git a/lib/model/settings/store/store.dart b/lib/model/settings/store/store.dart new file mode 100644 index 000000000..b12fa639f --- /dev/null +++ b/lib/model/settings/store/store.dart @@ -0,0 +1,37 @@ +abstract class SettingsStore { + bool get initialized; + + Future init(); + + Future clear(); + + Future remove(String key); + + // get + + Set getKeys(); + + Object? get(String key); + + bool? getBool(String key); + + int? getInt(String key); + + double? getDouble(String key); + + String? getString(String key); + + List? getStringList(String key); + + // set + + Future setBool(String key, bool value); + + Future setInt(String key, int value); + + Future setDouble(String key, double value); + + Future setString(String key, String value); + + Future setStringList(String key, List value); +} diff --git a/lib/model/settings/store/store_shared_pref.dart b/lib/model/settings/store/store_shared_pref.dart new file mode 100644 index 000000000..d7d6fd6fd --- /dev/null +++ b/lib/model/settings/store/store_shared_pref.dart @@ -0,0 +1,65 @@ +import 'package:aves/model/settings/store/store.dart'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPrefSettingsStore implements SettingsStore { + static SharedPreferences? _prefs; + + @override + bool get initialized => _prefs != null; + + @override + Future init() async { + try { + _prefs = await SharedPreferences.getInstance(); + } catch (error, stack) { + debugPrint('$runtimeType init error=$error\n$stack'); + } + } + + @override + Future clear() => _prefs!.clear(); + + @override + Future remove(String key) => _prefs!.remove(key); + + // get + + @override + Set getKeys() => _prefs!.getKeys(); + + @override + Object? get(String key) => _prefs!.get(key); + + @override + bool? getBool(String key) => _prefs!.getBool(key); + + @override + int? getInt(String key) => _prefs!.getInt(key); + + @override + double? getDouble(String key) => _prefs!.getDouble(key); + + @override + String? getString(String key) => _prefs!.getString(key); + + @override + List? getStringList(String key) => _prefs!.getStringList(key); + + // set + + @override + Future setBool(String key, bool value) => _prefs!.setBool(key, value); + + @override + Future setInt(String key, int value) => _prefs!.setInt(key, value); + + @override + Future setDouble(String key, double value) => _prefs!.setDouble(key, value); + + @override + Future setString(String key, String value) => _prefs!.setString(key, value); + + @override + Future setStringList(String key, List value) => _prefs!.setStringList(key, value); +} diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 25fe1eb68..88aa723d6 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -26,56 +26,9 @@ mixin AlbumMixin on SourceBase { return compareAsciiUpperCase(va, vb); } - void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent()); - - String getAlbumDisplayName(BuildContext? context, String dirPath) { - final separator = pContext.separator; - assert(!dirPath.endsWith(separator)); - - if (context != null) { - final type = androidFileUtils.getAlbumType(dirPath); - if (type == AlbumType.camera) return context.l10n.albumCamera; - if (type == AlbumType.download) return context.l10n.albumDownload; - if (type == AlbumType.screenshots) return context.l10n.albumScreenshots; - if (type == AlbumType.screenRecordings) return context.l10n.albumScreenRecordings; - if (type == AlbumType.videoCaptures) return context.l10n.albumVideoCaptures; - } - - final dir = VolumeRelativeDirectory.fromPath(dirPath); - if (dir == null) return dirPath; - - final relativeDir = dir.relativeDir; - if (relativeDir.isEmpty) { - final volume = androidFileUtils.getStorageVolume(dirPath)!; - return volume.getDescription(context); - } - - String unique(String dirPath, Set others) { - final parts = pContext.split(dirPath); - for (var i = parts.length - 1; i > 0; i--) { - final name = pContext.joinAll(['', ...parts.skip(i)]); - final testName = '$separator$name'; - if (others.every((item) => !item!.endsWith(testName))) return name; - } - return dirPath; - } - - final otherAlbumsOnDevice = _directories.where((item) => item != dirPath).toSet(); - final uniqueNameInDevice = unique(dirPath, otherAlbumsOnDevice); - if (uniqueNameInDevice.length <= relativeDir.length) { - return uniqueNameInDevice; - } - - final volumePath = dir.volumePath; - String trimVolumePath(String? path) => path!.substring(dir.volumePath.length); - final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path!.startsWith(volumePath)).map(trimVolumePath).toSet(); - final uniqueNameInVolume = unique(trimVolumePath(dirPath), otherAlbumsOnVolume); - final volume = androidFileUtils.getStorageVolume(dirPath)!; - if (volume.isPrimary) { - return uniqueNameInVolume; - } else { - return '$uniqueNameInVolume (${volume.getDescription(context)})'; - } + void _onAlbumChanged() { + invalidateAlbumDisplayNames(); + eventBus.fire(AlbumsChangedEvent()); } Map getAlbumEntries() { @@ -109,7 +62,7 @@ mixin AlbumMixin on SourceBase { void addDirectories(Set albums) { if (!_directories.containsAll(albums)) { _directories.addAll(albums); - _notifyAlbumChange(); + _onAlbumChanged(); } } @@ -117,7 +70,7 @@ mixin AlbumMixin on SourceBase { final emptyAlbums = (albums ?? _directories).where((v) => _isEmptyAlbum(v) && !_newAlbums.contains(v)).toSet(); if (emptyAlbums.isNotEmpty) { _directories.removeAll(emptyAlbums); - _notifyAlbumChange(); + _onAlbumChanged(); invalidateAlbumFilterSummary(directories: emptyAlbums); final bookmarks = settings.drawerAlbumBookmarks; @@ -166,6 +119,8 @@ mixin AlbumMixin on SourceBase { return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); } + // new albums + void createAlbum(String directory) { _newAlbums.add(directory); addDirectories({directory}); @@ -181,6 +136,80 @@ mixin AlbumMixin on SourceBase { void forgetNewAlbums(Set directories) { _newAlbums.removeAll(directories); } + + // display names + + final Map _albumDisplayNamesWithContext = {}, _albumDisplayNamesWithoutContext = {}; + + void invalidateAlbumDisplayNames() { + _albumDisplayNamesWithContext.clear(); + _albumDisplayNamesWithoutContext.clear(); + } + + String _computeDisplayName(BuildContext? context, String dirPath) { + final separator = pContext.separator; + assert(!dirPath.endsWith(separator)); + + if (context != null) { + final type = androidFileUtils.getAlbumType(dirPath); + switch (type) { + case AlbumType.camera: + return context.l10n.albumCamera; + case AlbumType.download: + return context.l10n.albumDownload; + case AlbumType.screenshots: + return context.l10n.albumScreenshots; + case AlbumType.screenRecordings: + return context.l10n.albumScreenRecordings; + case AlbumType.videoCaptures: + return context.l10n.albumVideoCaptures; + case AlbumType.regular: + case AlbumType.app: + break; + } + } + + final dir = VolumeRelativeDirectory.fromPath(dirPath); + if (dir == null) return dirPath; + + final relativeDir = dir.relativeDir; + if (relativeDir.isEmpty) { + final volume = androidFileUtils.getStorageVolume(dirPath)!; + return volume.getDescription(context); + } + + String unique(String dirPath, Set others) { + final parts = pContext.split(dirPath); + for (var i = parts.length - 1; i > 0; i--) { + final name = pContext.joinAll(['', ...parts.skip(i)]); + final testName = '$separator$name'; + if (others.every((item) => !item!.endsWith(testName))) return name; + } + return dirPath; + } + + final otherAlbumsOnDevice = _directories.where((item) => item != dirPath).toSet(); + final uniqueNameInDevice = unique(dirPath, otherAlbumsOnDevice); + if (uniqueNameInDevice.length <= relativeDir.length) { + return uniqueNameInDevice; + } + + final volumePath = dir.volumePath; + String trimVolumePath(String? path) => path!.substring(dir.volumePath.length); + final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path!.startsWith(volumePath)).map(trimVolumePath).toSet(); + final uniqueNameInVolume = unique(trimVolumePath(dirPath), otherAlbumsOnVolume); + final volume = androidFileUtils.getStorageVolume(dirPath)!; + if (volume.isPrimary) { + return uniqueNameInVolume; + } else { + return '$uniqueNameInVolume (${volume.getDescription(context)})'; + } + } + + String getAlbumDisplayName(BuildContext? context, String dirPath) { + final names = (context != null ? _albumDisplayNamesWithContext : _albumDisplayNamesWithoutContext); + return names.putIfAbsent(dirPath, () => _computeDisplayName(context, dirPath)); + } } class AlbumsChangedEvent {} diff --git a/lib/model/source/analysis_controller.dart b/lib/model/source/analysis_controller.dart index aeac9dd31..c4e1e9e41 100644 --- a/lib/model/source/analysis_controller.dart +++ b/lib/model/source/analysis_controller.dart @@ -2,12 +2,12 @@ import 'package:flutter/foundation.dart'; class AnalysisController { final bool canStartService, force; - final List? contentIds; + final List? entryIds; final ValueNotifier stopSignal; AnalysisController({ this.canStartService = true, - this.contentIds, + this.entryIds, this.force = false, ValueNotifier? stopSignal, }) : stopSignal = stopSignal ?? ValueNotifier(false); diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index f94a085a8..83cc962d8 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -10,6 +10,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/rating.dart'; +import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/events.dart'; @@ -31,7 +32,7 @@ class CollectionLens with ChangeNotifier { final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier(); final List _subscriptions = []; int? id; - bool listenToSource; + bool listenToSource, groupBursts; List? fixedSelection; List _filteredSortedEntries = []; @@ -43,6 +44,7 @@ class CollectionLens with ChangeNotifier { Set? filters, this.id, this.listenToSource = true, + this.groupBursts = true, this.fixedSelection, }) : filters = (filters ?? {}).whereNotNull().toSet(), sectionFactor = settings.collectionSectionFactor, @@ -53,9 +55,18 @@ class CollectionLens with ChangeNotifier { _subscriptions.add(sourceEvents.on().listen((e) => _onEntryAdded(e.entries))); _subscriptions.add(sourceEvents.on().listen((e) => _onEntryRemoved(e.entries))); _subscriptions.add(sourceEvents.on().listen((e) { - if (e.type == MoveType.move) { - // refreshing copied items is already handled via `EntryAddedEvent`s - _refresh(); + switch (e.type) { + case MoveType.copy: + case MoveType.export: + // refreshing new items is already handled via `EntryAddedEvent`s + break; + case MoveType.move: + case MoveType.fromBin: + _refresh(); + break; + case MoveType.toBin: + _onEntryRemoved(e.entries); + break; } })); _subscriptions.add(sourceEvents.on().listen((e) => _refresh())); @@ -68,7 +79,12 @@ class CollectionLens with ChangeNotifier { })); favourites.addListener(_onFavouritesChanged); } - settings.addListener(_onSettingsChanged); + _subscriptions.add(settings.updateStream + .where((event) => [ + Settings.collectionSortFactorKey, + Settings.collectionGroupFactorKey, + ].contains(event.key)) + .listen((_) => _onSettingsChanged())); _refresh(); } @@ -78,7 +94,6 @@ class CollectionLens with ChangeNotifier { ..forEach((sub) => sub.cancel()) ..clear(); favourites.removeListener(_onFavouritesChanged); - settings.removeListener(_onSettingsChanged); super.dispose(); } @@ -160,10 +175,8 @@ class CollectionLens with ChangeNotifier { filterChangeNotifier.notifyListeners(); } - final bool groupBursts = true; - void _applyFilters() { - final entries = fixedSelection ?? source.visibleEntries; + final entries = fixedSelection ?? (filters.contains(TrashFilter.instance) ? source.trashedEntries : source.visibleEntries); _filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.every((filter) => filter.test(entry)))); if (groupBursts) { diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 4c1591ba3..eade5b709 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -8,6 +8,8 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/tag.dart'; +import 'package:aves/model/filters/trash.dart'; +import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/analysis_controller.dart'; @@ -15,6 +17,7 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/events.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; +import 'package:aves/model/source/trash.dart'; import 'package:aves/services/analysis_service.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; @@ -22,13 +25,17 @@ import 'package:collection/collection.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; +enum SourceInitializationState { none, directory, full } + mixin SourceBase { EventBus get eventBus; - Map get entryById; + Map get entryById; Set get visibleEntries; + Set get trashedEntries; + List get sortedEntriesByDate; ValueNotifier stateNotifier = ValueNotifier(SourceState.ready); @@ -38,22 +45,33 @@ mixin SourceBase { void setProgress({required int done, required int total}) => progressNotifier.value = ProgressEvent(done: done, total: total); } -abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { +abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin, TrashMixin { + CollectionSource() { + settings.updateStream.where((event) => event.key == Settings.localeKey).listen((_) => invalidateAlbumDisplayNames()); + settings.updateStream.where((event) => event.key == Settings.hiddenFiltersKey).listen((event) { + final oldValue = event.oldValue; + if (oldValue is List?) { + final oldHiddenFilters = (oldValue ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); + _onFilterVisibilityChanged(oldHiddenFilters, settings.hiddenFilters); + } + }); + } + final EventBus _eventBus = EventBus(); @override EventBus get eventBus => _eventBus; - final Map _entryById = {}; + final Map _entryById = {}; @override - Map get entryById => Map.unmodifiable(_entryById); + Map get entryById => Map.unmodifiable(_entryById); final Set _rawEntries = {}; Set get allEntries => Set.unmodifiable(_rawEntries); - Set? _visibleEntries; + Set? _visibleEntries, _trashedEntries; @override Set get visibleEntries { @@ -61,6 +79,12 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return _visibleEntries!; } + @override + Set get trashedEntries { + _trashedEntries ??= Set.unmodifiable(_applyTrashFilter(_rawEntries)); + return _trashedEntries!; + } + List? _sortedEntriesByDate; @override @@ -69,6 +93,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return _sortedEntriesByDate!; } + // known date by entry ID late Map _savedDates; Future loadDates() async { @@ -76,12 +101,20 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } Iterable _applyHiddenFilters(Iterable entries) { - final hiddenFilters = settings.hiddenFilters; - return hiddenFilters.isEmpty ? entries : entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry))); + final hiddenFilters = { + TrashFilter.instance, + ...settings.hiddenFilters, + }; + return entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry))); + } + + Iterable _applyTrashFilter(Iterable entries) { + return entries.where(TrashFilter.instance.test); } void _invalidate([Set? entries]) { _visibleEntries = null; + _trashedEntries = null; _sortedEntriesByDate = null; invalidateAlbumFilterSummary(entries: entries); invalidateCountryFilterSummary(entries: entries); @@ -100,14 +133,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM void addEntries(Set entries) { if (entries.isEmpty) return; - final newIdMapEntries = Map.fromEntries(entries.map((v) => MapEntry(v.contentId, v))); + final newIdMapEntries = Map.fromEntries(entries.map((entry) => MapEntry(entry.id, entry))); if (_rawEntries.isNotEmpty) { - final newContentIds = newIdMapEntries.keys.toSet(); - _rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId)); + final newIds = newIdMapEntries.keys.toSet(); + _rawEntries.removeWhere((entry) => newIds.contains(entry.id)); } entries.where((entry) => entry.catalogDateMillis == null).forEach((entry) { - entry.catalogDateMillis = _savedDates[entry.contentId]; + entry.catalogDateMillis = _savedDates[entry.id]; }); _entryById.addAll(newIdMapEntries); @@ -118,14 +151,21 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM eventBus.fire(EntryAddedEvent(entries)); } - Future removeEntries(Set uris) async { + Future removeEntries(Set uris, {required bool includeTrash}) async { if (uris.isEmpty) return; - final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet(); - await favourites.remove(entries); - await covers.removeEntries(entries); - await metadataDb.removeVideoPlayback(entries.map((entry) => entry.contentId).whereNotNull().toSet()); - entries.forEach((v) => _entryById.remove(v.contentId)); + final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet(); + if (!includeTrash) { + entries.removeWhere(TrashFilter.instance.test); + } + if (entries.isEmpty) return; + + final ids = entries.map((entry) => entry.id).toSet(); + await favourites.removeIds(ids); + await covers.removeIds(ids); + await metadataDb.removeIds(ids); + + ids.forEach((id) => _entryById.remove); _rawEntries.removeAll(entries); updateDerivedFilters(entries); eventBus.fire(EntryRemovedEvent(entries)); @@ -142,27 +182,51 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } Future _moveEntry(AvesEntry entry, Map newFields, {required bool persist}) async { - final oldContentId = entry.contentId!; - final newContentId = newFields['contentId'] as int?; + newFields.keys.forEach((key) { + switch (key) { + case 'contentId': + entry.contentId = newFields['contentId'] as int?; + break; + case 'dateModifiedSecs': + // `dateModifiedSecs` changes when moving entries to another directory, + // but it does not change when renaming the containing directory + entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int?; + break; + case 'path': + entry.path = newFields['path'] as String?; + break; + case 'title': + entry.sourceTitle = newFields['title'] as String?; + break; + case 'trashed': + final trashed = newFields['trashed'] as bool; + entry.trashed = trashed; + entry.trashDetails = trashed + ? TrashDetails( + id: entry.id, + path: newFields['trashPath'] as String, + dateMillis: DateTime.now().millisecondsSinceEpoch, + ) + : null; + break; + case 'uri': + entry.uri = newFields['uri'] as String; + break; + } + }); + if (entry.trashed) { + entry.contentId = null; + entry.uri = 'file://${entry.trashDetails?.path}'; + } - entry.contentId = newContentId; - // `dateModifiedSecs` changes when moving entries to another directory, - // but it does not change when renaming the containing directory - if (newFields.containsKey('dateModifiedSecs')) entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int?; - if (newFields.containsKey('path')) entry.path = newFields['path'] as String?; - if (newFields.containsKey('uri')) entry.uri = newFields['uri'] as String; - if (newFields.containsKey('title')) entry.sourceTitle = newFields['title'] as String?; - - entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId); - entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId); + await covers.moveEntry(entry, persist: persist); if (persist) { - await metadataDb.updateEntryId(oldContentId, entry); - await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata); - await metadataDb.updateAddressId(oldContentId, entry.addressDetails); - await favourites.moveEntry(oldContentId, entry); - await covers.moveEntry(oldContentId, entry); - await metadataDb.updateVideoPlaybackId(oldContentId, entry.contentId); + final id = entry.id; + await metadataDb.updateEntry(id, entry); + await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata); + await metadataDb.updateAddress(id, entry.addressDetails); + await metadataDb.updateTrash(id, entry.trashDetails); } } @@ -198,42 +262,40 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return success; } - Future renameAlbum(String sourceAlbum, String destinationAlbum, Set todoEntries, Set movedOps) async { + Future renameAlbum(String sourceAlbum, String destinationAlbum, Set entries, Set movedOps) async { final oldFilter = AlbumFilter(sourceAlbum, null); - final bookmarked = settings.drawerAlbumBookmarks?.contains(sourceAlbum) == true; + final newFilter = AlbumFilter(destinationAlbum, null); + + final bookmark = settings.drawerAlbumBookmarks?.indexOf(sourceAlbum); final pinned = settings.pinnedFilters.contains(oldFilter); - final oldCoverContentId = covers.coverContentId(oldFilter); - final coverEntry = oldCoverContentId != null ? todoEntries.firstWhereOrNull((entry) => entry.contentId == oldCoverContentId) : null; + await covers.set(newFilter, covers.coverEntryId(oldFilter)); renameNewAlbum(sourceAlbum, destinationAlbum); await updateAfterMove( - todoEntries: todoEntries, - copy: false, - destinationAlbum: destinationAlbum, + todoEntries: entries, + moveType: MoveType.move, + destinationAlbums: {destinationAlbum}, movedOps: movedOps, ); - // restore bookmark, pin and cover, as the obsolete album got removed and its associated state cleaned - final newFilter = AlbumFilter(destinationAlbum, null); - if (bookmarked) { - settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..add(destinationAlbum); + // restore bookmark and pin, as the obsolete album got removed and its associated state cleaned + if (bookmark != null && bookmark != -1) { + settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..insert(bookmark, destinationAlbum); } if (pinned) { settings.pinnedFilters = settings.pinnedFilters..add(newFilter); } - if (coverEntry != null) { - await covers.set(newFilter, coverEntry.contentId); - } } Future updateAfterMove({ required Set todoEntries, - required bool copy, - required String destinationAlbum, + required MoveType moveType, + required Set destinationAlbums, required Set movedOps, }) async { if (movedOps.isEmpty) return; final fromAlbums = {}; final movedEntries = {}; + final copy = moveType == MoveType.copy; if (copy) { movedOps.forEach((movedOp) { final sourceUri = movedOp.uri; @@ -242,6 +304,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM if (sourceEntry != null) { fromAlbums.add(sourceEntry.directory); movedEntries.add(sourceEntry.copyWith( + id: metadataDb.nextId, uri: newFields['uri'] as String?, path: newFields['path'] as String?, contentId: newFields['contentId'] as int?, @@ -254,7 +317,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } }); await metadataDb.saveEntries(movedEntries); - await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata).whereNotNull().toSet()); + await metadataDb.saveCatalogMetadata(movedEntries.map((entry) => entry.catalogMetadata).whereNotNull().toSet()); await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).whereNotNull().toSet()); } else { await Future.forEach(movedOps, (movedOp) async { @@ -263,7 +326,11 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM final sourceUri = movedOp.uri; final entry = todoEntries.firstWhereOrNull((entry) => entry.uri == sourceUri); if (entry != null) { - fromAlbums.add(entry.directory); + if (moveType == MoveType.fromBin) { + newFields['trashed'] = false; + } else { + fromAlbums.add(entry.directory); + } movedEntries.add(entry); await _moveEntry(entry, newFields, persist: true); } @@ -275,18 +342,22 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM addEntries(movedEntries); } else { cleanEmptyAlbums(fromAlbums); - addDirectories({destinationAlbum}); + if (moveType != MoveType.toBin) { + addDirectories(destinationAlbums); + } } invalidateAlbumFilterSummary(directories: fromAlbums); _invalidate(movedEntries); - eventBus.fire(EntryMovedEvent(copy ? MoveType.copy : MoveType.move, movedEntries)); + eventBus.fire(EntryMovedEvent(moveType, movedEntries)); } - bool get initialized => false; + SourceInitializationState get initState => SourceInitializationState.none; - Future init(); - - Future refresh({AnalysisController? analysisController}); + Future init({ + AnalysisController? analysisController, + String? directory, + bool loadTopEntriesFirst = false, + }); Future> refreshUris(Set changedUris, {AnalysisController? analysisController}); @@ -294,13 +365,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM await entry.refresh(background: false, persist: true, dataTypes: dataTypes, geocoderLocale: settings.appliedLocale); // update/delete in DB - final contentId = entry.contentId!; + final id = entry.id; if (dataTypes.contains(EntryDataType.catalog)) { - await metadataDb.updateMetadataId(contentId, entry.catalogMetadata); + await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata); onCatalogMetadataChanged(); } if (dataTypes.contains(EntryDataType.address)) { - await metadataDb.updateAddressId(contentId, entry.addressDetails); + await metadataDb.updateAddress(id, entry.addressDetails); onAddressMetadataChanged(); } @@ -334,7 +405,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM if (startAnalysisService) { await AnalysisService.startService( force: force, - contentIds: entries?.map((entry) => entry.contentId).whereNotNull().toList(), + entryIds: entries?.map((entry) => entry.id).toList(), ); } else { await catalogEntries(_analysisController, todoEntries); @@ -373,28 +444,21 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } AvesEntry? coverEntry(CollectionFilter filter) { - final contentId = covers.coverContentId(filter); - if (contentId != null) { - final entry = visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId); + final id = covers.coverEntryId(filter); + if (id != null) { + final entry = visibleEntries.firstWhereOrNull((entry) => entry.id == id); if (entry != null) return entry; } return recentEntry(filter); } - void changeFilterVisibility(Set filters, bool visible) { - final hiddenFilters = settings.hiddenFilters; - if (visible) { - hiddenFilters.removeAll(filters); - } else { - hiddenFilters.addAll(filters); - settings.searchHistory = settings.searchHistory..removeWhere(filters.contains); - } - settings.hiddenFilters = hiddenFilters; + void _onFilterVisibilityChanged(Set oldHiddenFilters, Set currentHiddenFilters) { updateDerivedFilters(); - eventBus.fire(FilterVisibilityChangedEvent(filters, visible)); + eventBus.fire(const FilterVisibilityChangedEvent()); - if (visible) { - final candidateEntries = visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet(); + final newlyVisibleFilters = oldHiddenFilters.whereNot(currentHiddenFilters.contains).toSet(); + if (newlyVisibleFilters.isNotEmpty) { + final candidateEntries = visibleEntries.where((entry) => newlyVisibleFilters.any((f) => f.test(entry))).toSet(); analyze(null, entries: candidateEntries); } } diff --git a/lib/model/source/events.dart b/lib/model/source/events.dart index 582a86c2f..eed59df63 100644 --- a/lib/model/source/events.dart +++ b/lib/model/source/events.dart @@ -1,6 +1,5 @@ import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/filters/filters.dart'; import 'package:flutter/foundation.dart'; @immutable @@ -34,10 +33,7 @@ class EntryRefreshedEvent { @immutable class FilterVisibilityChangedEvent { - final Set filters; - final bool visible; - - const FilterVisibilityChangedEvent(this.filters, this.visible); + const FilterVisibilityChangedEvent(); } @immutable diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index dbabb7954..deca24b85 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -20,10 +20,10 @@ mixin LocationMixin on SourceBase { List sortedCountries = List.unmodifiable([]); List sortedPlaces = List.unmodifiable([]); - Future loadAddresses() async { - final saved = await metadataDb.loadAllAddresses(); + Future loadAddresses({Set? ids}) async { + final saved = await (ids != null ? metadataDb.loadAddressesById(ids) : metadataDb.loadAddresses()); final idMap = entryById; - saved.forEach((metadata) => idMap[metadata.contentId]?.addressDetails = metadata); + saved.forEach((metadata) => idMap[metadata.id]?.addressDetails = metadata); onAddressMetadataChanged(); } @@ -31,7 +31,7 @@ mixin LocationMixin on SourceBase { await _locateCountries(controller, candidateEntries); await _locatePlaces(controller, candidateEntries); - final unlocatedIds = candidateEntries.where((entry) => !entry.hasGps).map((entry) => entry.contentId).whereNotNull().toSet(); + final unlocatedIds = candidateEntries.where((entry) => !entry.hasGps).map((entry) => entry.id).toSet(); if (unlocatedIds.isNotEmpty) { await metadataDb.removeIds(unlocatedIds, dataTypes: {EntryDataType.address}); onAddressMetadataChanged(); @@ -115,7 +115,7 @@ mixin LocationMixin on SourceBase { for (final entry in todo) { final latLng = approximateLatLng(entry); if (knownLocations.containsKey(latLng)) { - entry.addressDetails = knownLocations[latLng]?.copyWith(contentId: entry.contentId); + entry.addressDetails = knownLocations[latLng]?.copyWith(id: entry.id); } else { await entry.locatePlace(background: true, force: force, geocoderLocale: settings.appliedLocale); // it is intended to insert `null` if the geocoder failed, diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 0a88b12d2..03d433eec 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -14,13 +14,31 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class MediaStoreSource extends CollectionSource { - bool _initialized = false; + SourceInitializationState _initState = SourceInitializationState.none; @override - bool get initialized => _initialized; + SourceInitializationState get initState => _initState; @override - Future init() async { + Future init({ + AnalysisController? analysisController, + String? directory, + bool loadTopEntriesFirst = false, + }) async { + if (_initState == SourceInitializationState.none) { + await _loadEssentials(); + } + if (_initState != SourceInitializationState.full) { + _initState = directory != null ? SourceInitializationState.directory : SourceInitializationState.full; + } + unawaited(_loadEntries( + analysisController: analysisController, + directory: directory, + loadTopEntriesFirst: loadTopEntriesFirst, + )); + } + + Future _loadEssentials() async { final stopwatch = Stopwatch()..start(); stateNotifier.value = SourceState.loading; await metadataDb.init(); @@ -33,52 +51,94 @@ class MediaStoreSource extends CollectionSource { // clear catalog metadata to get correct date/times when moving to a different time zone debugPrint('$runtimeType clear catalog metadata to get correct date/times'); await metadataDb.clearDates(); - await metadataDb.clearMetadataEntries(); + await metadataDb.clearCatalogMetadata(); settings.catalogTimeZone = currentTimeZone; } } await loadDates(); - _initialized = true; - debugPrint('$runtimeType init complete in ${stopwatch.elapsed.inMilliseconds}ms'); + debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms'); } - @override - Future refresh({AnalysisController? analysisController}) async { - assert(_initialized); + Future _loadEntries({ + AnalysisController? analysisController, + String? directory, + required bool loadTopEntriesFirst, + }) async { debugPrint('$runtimeType refresh start'); final stopwatch = Stopwatch()..start(); stateNotifier.value = SourceState.loading; clearEntries(); + final Set topEntries = {}; + if (loadTopEntriesFirst) { + final topIds = settings.topEntryIds; + if (topIds != null) { + debugPrint('$runtimeType refresh ${stopwatch.elapsed} load ${topIds.length} top entries'); + topEntries.addAll(await metadataDb.loadEntriesById(topIds)); + addEntries(topEntries); + } + } + debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch known entries'); - final oldEntries = await metadataDb.loadAllEntries(); + final knownEntries = await metadataDb.loadEntries(directory: directory); + final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet(); + debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete entries'); - final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId!, entry.dateModifiedSecs!))); - final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet(); - oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId)); + final knownDateByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs))); + final knownContentIds = knownDateByContentId.keys.toList(); + final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet(); + if (topEntries.isNotEmpty) { + final removedTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId)); + await removeEntries(removedTopEntries.map((entry) => entry.uri).toSet(), includeTrash: false); + } + final removedEntries = knownEntries.where((entry) => removedContentIds.contains(entry.contentId)).toSet(); + knownEntries.removeAll(removedEntries); // show known entries debugPrint('$runtimeType refresh ${stopwatch.elapsed} add known entries'); - addEntries(oldEntries); + addEntries(knownEntries); + debugPrint('$runtimeType refresh ${stopwatch.elapsed} load metadata'); - await loadCatalogMetadata(); - await loadAddresses(); - updateDerivedFilters(); + if (directory != null) { + final ids = knownLiveEntries.map((entry) => entry.id).toSet(); + await loadCatalogMetadata(ids: ids); + await loadAddresses(ids: ids); + } else { + await loadCatalogMetadata(); + await loadAddresses(); + updateDerivedFilters(); + } // clean up obsolete entries - debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries'); - await metadataDb.removeIds(obsoleteContentIds); + if (removedEntries.isNotEmpty) { + debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries'); + await metadataDb.removeIds(removedEntries.map((entry) => entry.id)); + } + + if (directory != null) { + // trash + await loadTrashDetails(); + unawaited(deleteExpiredTrash().then( + (deletedUris) { + if (deletedUris.isNotEmpty) { + debugPrint('evicted ${deletedUris.length} expired items from the trash'); + removeEntries(deletedUris, includeTrash: true); + } + }, + onError: (error) => debugPrint('failed to evict expired trash error=$error'), + )); + } // verify paths because some apps move files without updating their `last modified date` debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete paths'); - final knownPathById = Map.fromEntries(allEntries.map((entry) => MapEntry(entry.contentId!, entry.path))); - final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathById)).toSet(); + final knownPathByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.path))); + final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathByContentId)).toSet(); movedContentIds.forEach((contentId) { // make obsolete by resetting its modified date - knownDateById[contentId] = 0; + knownDateByContentId[contentId] = 0; }); - // fetch new entries + // fetch new & modified entries debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch new entries'); // refresh after the first 10 entries, then after 100 more, then every 1000 entries var refreshCount = 10; @@ -90,8 +150,13 @@ class MediaStoreSource extends CollectionSource { pendingNewEntries.clear(); } - mediaStoreService.getEntries(knownDateById).listen( + mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen( (entry) { + // when discovering modified entry with known content ID, + // reuse known entry ID to overwrite it while preserving favourites, etc. + final contentId = entry.contentId; + entry.id = (knownContentIds.contains(contentId) ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId)?.id : null) ?? metadataDb.nextId; + pendingNewEntries.add(entry); if (pendingNewEntries.length >= refreshCount) { refreshCount = min(refreshCount * 10, refreshCountMax); @@ -111,14 +176,15 @@ class MediaStoreSource extends CollectionSource { updateDirectories(); } + debugPrint('$runtimeType refresh ${stopwatch.elapsed} analyze'); Set? analysisEntries; - final analysisIds = analysisController?.contentIds; + final analysisIds = analysisController?.entryIds; if (analysisIds != null) { - analysisEntries = visibleEntries.where((entry) => analysisIds.contains(entry.contentId)).toSet(); + analysisEntries = visibleEntries.where((entry) => analysisIds.contains(entry.id)).toSet(); } await analyze(analysisController, entries: analysisEntries); - debugPrint('$runtimeType refresh ${stopwatch.elapsed} done for ${oldEntries.length} known, ${allNewEntries.length} new, ${obsoleteContentIds.length} obsolete'); + debugPrint('$runtimeType refresh ${stopwatch.elapsed} done for ${knownEntries.length} known, ${allNewEntries.length} new, ${removedEntries.length} removed'); }, onError: (error) => debugPrint('$runtimeType stream error=$error'), ); @@ -131,7 +197,7 @@ class MediaStoreSource extends CollectionSource { // sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg` @override Future> refreshUris(Set changedUris, {AnalysisController? analysisController}) async { - if (!_initialized || !isMonitoring) return changedUris; + if (_initState == SourceInitializationState.none || !isMonitoring) return changedUris; debugPrint('$runtimeType refreshUris ${changedUris.length} uris'); final uriByContentId = Map.fromEntries(changedUris.map((uri) { @@ -147,7 +213,7 @@ class MediaStoreSource extends CollectionSource { // clean up obsolete entries final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(uriByContentId.keys.toList())).toSet(); final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).whereNotNull().toSet(); - await removeEntries(obsoleteUris); + await removeEntries(obsoleteUris, includeTrash: false); obsoleteContentIds.forEach(uriByContentId.remove); // fetch new entries @@ -165,6 +231,7 @@ class MediaStoreSource extends CollectionSource { final newPath = sourceEntry.path; final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null; if (volume != null) { + sourceEntry.id = existingEntry?.id ?? metadataDb.nextId; newEntries.add(sourceEntry); final existingDirectory = existingEntry?.directory; if (existingDirectory != null) { diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 16583cc2a..b2fa61907 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -14,10 +14,10 @@ mixin TagMixin on SourceBase { List sortedTags = List.unmodifiable([]); - Future loadCatalogMetadata() async { - final saved = await metadataDb.loadAllMetadataEntries(); + Future loadCatalogMetadata({Set? ids}) async { + final saved = await (ids != null ? metadataDb.loadCatalogMetadataById(ids) : metadataDb.loadCatalogMetadata()); final idMap = entryById; - saved.forEach((metadata) => idMap[metadata.contentId]?.catalogMetadata = metadata); + saved.forEach((metadata) => idMap[metadata.id]?.catalogMetadata = metadata); onCatalogMetadataChanged(); } @@ -42,7 +42,7 @@ mixin TagMixin on SourceBase { if (entry.isCatalogued) { newMetadata.add(entry.catalogMetadata!); if (newMetadata.length >= commitCountThreshold) { - await metadataDb.saveMetadata(Set.unmodifiable(newMetadata)); + await metadataDb.saveCatalogMetadata(Set.unmodifiable(newMetadata)); onCatalogMetadataChanged(); newMetadata.clear(); } @@ -53,7 +53,7 @@ mixin TagMixin on SourceBase { } setProgress(done: ++progressDone, total: progressTotal); } - await metadataDb.saveMetadata(Set.unmodifiable(newMetadata)); + await metadataDb.saveCatalogMetadata(Set.unmodifiable(newMetadata)); onCatalogMetadataChanged(); } diff --git a/lib/model/source/trash.dart b/lib/model/source/trash.dart new file mode 100644 index 000000000..cbd2fd3ad --- /dev/null +++ b/lib/model/source/trash.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/services/common/image_op_events.dart'; +import 'package:aves/services/common/services.dart'; + +mixin TrashMixin on SourceBase { + static const Duration binKeepDuration = Duration(days: 30); + + Future loadTrashDetails() async { + final saved = await metadataDb.loadAllTrashDetails(); + final idMap = entryById; + saved.forEach((details) => idMap[details.id]?.trashDetails = details); + } + + Future> deleteExpiredTrash() async { + final expiredEntries = trashedEntries.where((entry) => entry.isExpiredTrash).toSet(); + if (expiredEntries.isEmpty) return {}; + + final processed = {}; + final completer = Completer>(); + mediaFileService.delete(entries: expiredEntries).listen( + processed.add, + onError: completer.completeError, + onDone: () async { + final successOps = processed.where((e) => e.success).toSet(); + final deletedOps = successOps.where((e) => !e.skipped).toSet(); + final deletedUris = deletedOps.map((event) => event.uri).toSet(); + completer.complete(deletedUris); + }, + ); + return await completer.future; + } +} diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index c83759bc4..1b7564ba3 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -99,7 +99,7 @@ class VideoMetadataFormatter { } if (dateMillis != null) { - return (entry.catalogMetadata ?? CatalogMetadata(contentId: entry.contentId)).copyWith( + return (entry.catalogMetadata ?? CatalogMetadata(id: entry.id)).copyWith( dateMillis: dateMillis, ); } diff --git a/lib/model/video_playback.dart b/lib/model/video_playback.dart index 660e4a77f..43bfce360 100644 --- a/lib/model/video_playback.dart +++ b/lib/model/video_playback.dart @@ -3,25 +3,25 @@ import 'package:flutter/foundation.dart'; @immutable class VideoPlaybackRow extends Equatable { - final int contentId, resumeTimeMillis; + final int entryId, resumeTimeMillis; @override - List get props => [contentId, resumeTimeMillis]; + List get props => [entryId, resumeTimeMillis]; const VideoPlaybackRow({ - required this.contentId, + required this.entryId, required this.resumeTimeMillis, }); static VideoPlaybackRow? fromMap(Map map) { return VideoPlaybackRow( - contentId: map['contentId'], + entryId: map['id'], resumeTimeMillis: map['resumeTimeMillis'], ); } Map toMap() => { - 'contentId': contentId, + 'id': entryId, 'resumeTimeMillis': resumeTimeMillis, }; } diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart index 978668f31..185165bec 100644 --- a/lib/ref/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -111,5 +111,6 @@ class MimeTypes { case '.svg': return svg; } + return null; } } diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index 89d52983f..95b8eb752 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -25,10 +25,10 @@ class AnalysisService { } } - static Future startService({required bool force, List? contentIds}) async { + static Future startService({required bool force, List? entryIds}) async { try { await platform.invokeMethod('startService', { - 'contentIds': contentIds, + 'entryIds': entryIds, 'force': force, }); } on PlatformException catch (e, stack) { @@ -98,16 +98,16 @@ class Analyzer { } Future start(dynamic args) async { - debugPrint('$runtimeType start'); - List? contentIds; + List? entryIds; var force = false; if (args is Map) { - contentIds = (args['contentIds'] as List?)?.cast(); + entryIds = (args['entryIds'] as List?)?.cast(); force = args['force'] ?? false; } + debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries'); _controller = AnalysisController( canStartService: false, - contentIds: contentIds, + entryIds: entryIds, force: force, stopSignal: ValueNotifier(false), ); @@ -115,8 +115,7 @@ class Analyzer { settings.systemLocalesFallback = await deviceService.getLocales(); _l10n = await AppLocalizations.delegate.load(settings.appliedLocale); _serviceStateNotifier.value = AnalyzerState.running; - await _source.init(); - unawaited(_source.refresh(analysisController: _controller)); + await _source.init(analysisController: _controller); _notificationUpdateTimer = Timer.periodic(notificationUpdateInterval, (_) async { if (!isRunning) return; diff --git a/lib/services/common/services.dart b/lib/services/common/services.dart index 3a877b3d8..12f817a61 100644 --- a/lib/services/common/services.dart +++ b/lib/services/common/services.dart @@ -1,5 +1,8 @@ import 'package:aves/model/availability.dart'; -import 'package:aves/model/metadata_db.dart'; +import 'package:aves/model/db/db_metadata.dart'; +import 'package:aves/model/db/db_metadata_sqflite.dart'; +import 'package:aves/model/settings/store/store.dart'; +import 'package:aves/model/settings/store/store_shared_pref.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/device_service.dart'; import 'package:aves/services/media/embedded_data_service.dart'; @@ -16,6 +19,9 @@ import 'package:path/path.dart' as p; final getIt = GetIt.instance; +// fixed implementation is easier for test driver setup +final SettingsStore settingsStore = SharedPrefSettingsStore(); + final p.Context pContext = getIt(); final AvesAvailability availability = getIt(); final MetadataDb metadataDb = getIt(); diff --git a/lib/services/media/media_file_service.dart b/lib/services/media/media_file_service.dart index a73e74319..46c3210ca 100644 --- a/lib/services/media/media_file_service.dart +++ b/lib/services/media/media_file_service.dart @@ -80,9 +80,8 @@ abstract class MediaFileService { Stream move({ String? opId, - required Iterable entries, + required Map> entriesByDestination, required bool copy, - required String destinationAlbum, required NameConflictStrategy nameConflictStrategy, }); @@ -126,6 +125,8 @@ class PlatformMediaFileService implements MediaFileService { 'isFlipped': entry.isFlipped, 'dateModifiedSecs': entry.dateModifiedSecs, 'sizeBytes': entry.sizeBytes, + 'trashed': entry.trashed, + 'trashPath': entry.trashDetails?.path, }; } @@ -343,9 +344,8 @@ class PlatformMediaFileService implements MediaFileService { @override Stream move({ String? opId, - required Iterable entries, + required Map> entriesByDestination, required bool copy, - required String destinationAlbum, required NameConflictStrategy nameConflictStrategy, }) { try { @@ -353,9 +353,8 @@ class PlatformMediaFileService implements MediaFileService { .receiveBroadcastStream({ 'op': 'move', 'id': opId, - 'entries': entries.map(_toPlatformEntryMap).toList(), + 'entriesByDestination': entriesByDestination.map((destination, entries) => MapEntry(destination, entries.map(_toPlatformEntryMap).toList())), 'copy': copy, - 'destinationPath': destinationAlbum, 'nameConflictStrategy': nameConflictStrategy.toPlatform(), }) .where((event) => event is Map) diff --git a/lib/services/media/media_store_service.dart b/lib/services/media/media_store_service.dart index 9bb493e92..8a6647485 100644 --- a/lib/services/media/media_store_service.dart +++ b/lib/services/media/media_store_service.dart @@ -6,12 +6,12 @@ import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; abstract class MediaStoreService { - Future> checkObsoleteContentIds(List knownContentIds); + Future> checkObsoleteContentIds(List knownContentIds); - Future> checkObsoletePaths(Map knownPathById); + Future> checkObsoletePaths(Map knownPathById); // knownEntries: map of contentId -> dateModifiedSecs - Stream getEntries(Map knownEntries); + Stream getEntries(Map knownEntries, {String? directory}); // returns media URI Future scanFile(String path, String mimeType); @@ -22,7 +22,7 @@ class PlatformMediaStoreService implements MediaStoreService { static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/media_store_stream'); @override - Future> checkObsoleteContentIds(List knownContentIds) async { + Future> checkObsoleteContentIds(List knownContentIds) async { try { final result = await platform.invokeMethod('checkObsoleteContentIds', { 'knownContentIds': knownContentIds, @@ -35,7 +35,7 @@ class PlatformMediaStoreService implements MediaStoreService { } @override - Future> checkObsoletePaths(Map knownPathById) async { + Future> checkObsoletePaths(Map knownPathById) async { try { final result = await platform.invokeMethod('checkObsoletePaths', { 'knownPathById': knownPathById, @@ -48,11 +48,12 @@ class PlatformMediaStoreService implements MediaStoreService { } @override - Stream getEntries(Map knownEntries) { + Stream getEntries(Map knownEntries, {String? directory}) { try { return _streamChannel .receiveBroadcastStream({ 'knownEntries': knownEntries, + 'directory': directory, }) .where((event) => event is Map) .map((event) => AvesEntry.fromMap(event as Map)); diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart index 40e6143bf..c3b397cc4 100644 --- a/lib/services/metadata/metadata_fetch_service.dart +++ b/lib/services/metadata/metadata_fetch_service.dart @@ -79,7 +79,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { 'path': entry.path, 'sizeBytes': entry.sizeBytes, }) as Map; - result['contentId'] = entry.contentId; + result['id'] = entry.id; return CatalogMetadata.fromMap(result); } on PlatformException catch (e, stack) { if (!entry.isMissingAtPath) { diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart new file mode 100644 index 000000000..e430be7a0 --- /dev/null +++ b/lib/theme/colors.dart @@ -0,0 +1,43 @@ +import 'package:aves/utils/color_utils.dart'; +import 'package:flutter/material.dart'; + +class AColors { + // mime + static final image = stringToColor('Image'); + static final video = stringToColor('Video'); + + // type + static const favourite = Colors.red; + static final animated = stringToColor('Animated'); + static final geotiff = stringToColor('GeoTIFF'); + static final motionPhoto = stringToColor('Motion Photo'); + static final panorama = stringToColor('Panorama'); + static final raw = stringToColor('Raw'); + static final sphericalVideo = stringToColor('360° Video'); + + // albums + static final albumCamera = stringToColor('Camera'); + static final albumDownload = stringToColor('Download'); + static final albumScreenshots = stringToColor('Screenshots'); + static final albumScreenRecordings = stringToColor('Screen recordings'); + static final albumVideoCaptures = stringToColor('Video Captures'); + + // info + static final xmp = stringToColor('XMP'); + + // settings + static final accessibility = stringToColor('Accessibility'); + static final language = stringToColor('Language'); + static final navigation = stringToColor('Navigation'); + static final privacy = stringToColor('Privacy'); + static final thumbnails = stringToColor('Thumbnails'); + + static const debugGradient = LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.red, + Colors.amber, + ], + ); +} diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index bed800993..4c91a1685 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/settings/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index c91a307b7..46a45c308 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -9,6 +9,7 @@ class AIcons { static const IconData accessibility = Icons.accessibility_new_outlined; static const IconData android = Icons.android; + static const IconData bin = Icons.delete_outlined; static const IconData broken = Icons.broken_image_outlined; static const IconData checked = Icons.done_outlined; static const IconData date = Icons.calendar_today_outlined; @@ -45,18 +46,20 @@ class AIcons { static const IconData add = Icons.add_circle_outline; static const IconData addShortcut = Icons.add_to_home_screen_outlined; static const IconData cancel = Icons.cancel_outlined; - static const IconData replay10 = Icons.replay_10_outlined; - static const IconData skip10 = Icons.forward_10_outlined; static const IconData captureFrame = Icons.screenshot_outlined; static const IconData clear = Icons.clear_outlined; static const IconData clipboard = Icons.content_copy_outlined; + static const IconData convert = Icons.transform_outlined; static const IconData copy = Icons.file_copy_outlined; static const IconData debug = Icons.whatshot_outlined; static const IconData delete = Icons.delete_outlined; static const IconData edit = Icons.edit_outlined; static const IconData editRating = MdiIcons.starPlusOutline; static const IconData editTags = MdiIcons.tagPlusOutline; - static const IconData export = MdiIcons.fileExportOutline; + static const IconData emptyBin = Icons.delete_sweep_outlined; + static const IconData export = Icons.open_with_outlined; + static const IconData fileExport = MdiIcons.fileExportOutline; + static const IconData fileImport = MdiIcons.fileImportOutline; static const IconData flip = Icons.flip_outlined; static const IconData favourite = Icons.favorite_border; static const IconData favouriteActive = Icons.favorite; @@ -65,7 +68,6 @@ class AIcons { static const IconData geoBounds = Icons.public_outlined; static const IconData goUp = Icons.arrow_upward_outlined; static const IconData hide = Icons.visibility_off_outlined; - static const IconData import = MdiIcons.fileImportOutline; static const IconData info = Icons.info_outlined; static const IconData layers = Icons.layers_outlined; static const IconData map = Icons.map_outlined; @@ -79,13 +81,16 @@ class AIcons { static const IconData print = Icons.print_outlined; static const IconData refresh = Icons.refresh_outlined; static const IconData rename = Icons.title_outlined; + static const IconData replay10 = Icons.replay_10_outlined; + static const IconData skip10 = Icons.forward_10_outlined; static const IconData reset = Icons.restart_alt_outlined; + static const IconData restore = Icons.restore_outlined; static const IconData rotateLeft = Icons.rotate_left_outlined; static const IconData rotateRight = Icons.rotate_right_outlined; static const IconData rotateScreen = Icons.screen_rotation_outlined; - static const IconData saveAs = Icons.save_alt_outlined; static const IconData search = Icons.search_outlined; static const IconData select = Icons.select_all_outlined; + static const IconData setAs = Icons.wallpaper_outlined; static const IconData setCover = MdiIcons.imageEditOutline; static const IconData share = Icons.share_outlined; static const IconData show = Icons.visibility_outlined; diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart index 5dd13fc3b..795c00371 100644 --- a/lib/theme/themes.dart +++ b/lib/theme/themes.dart @@ -5,15 +5,6 @@ import 'package:flutter/material.dart'; class Themes { static const _accentColor = Colors.indigoAccent; - static const debugGradient = LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.red, - Colors.amber, - ], - ); - static final darkTheme = ThemeData( brightness: Brightness.dark, // canvas color is used as background for the drawer and popups diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index c0de12a18..429692b40 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -8,6 +8,8 @@ import 'package:flutter/widgets.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { + static const String trashDirPath = '#trash'; + late final String separator, primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath; late final Set videoCapturesPaths; Set storageVolumes = {}; diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart index b3699462f..f448aef68 100644 --- a/lib/utils/time_utils.dart +++ b/lib/utils/time_utils.dart @@ -36,6 +36,7 @@ DateTime? dateTimeFromMillis(int? millis, {bool isUtc = false}) { millis = int.tryParse('$millis'.substring(0, _millisMaxDigits)); return dateTimeFromMillis(millis, isUtc: isUtc); } + return null; } final _unixStampMillisPattern = RegExp(r'\d{13}'); @@ -93,4 +94,6 @@ DateTime? parseUnknownDateFormat(String? s) { } } } + + return null; } diff --git a/lib/utils/xmp_utils.dart b/lib/utils/xmp_utils.dart index d897e476a..88e153dd4 100644 --- a/lib/utils/xmp_utils.dart +++ b/lib/utils/xmp_utils.dart @@ -283,6 +283,7 @@ class XMP { case -1: return '-1'; } + return null; } } diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart index 67ef9be7c..83b89e9a2 100644 --- a/lib/widgets/about/credits.dart +++ b/lib/widgets/about/credits.dart @@ -7,6 +7,7 @@ class AboutCredits extends StatelessWidget { const AboutCredits({Key? key}) : super(key: key); static const translators = { + 'Bahasa Indonesia': 'MeFinity', 'Deutsch': 'JanWaldhorn', 'Español (México)': 'n-berenice', 'Português (Brasil)': 'Jonatas De Almeida Barros', diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 995ad2040..db11d41c1 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -1,12 +1,14 @@ import 'dart:async'; +import 'dart:ui'; import 'package:aves/app_flavor.dart'; import 'package:aves/app_mode.dart'; import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/device.dart'; -import 'package:aves/model/settings/accessibility_animations.dart'; -import 'package:aves/model/settings/screen_on.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/services/accessibility_service.dart'; @@ -16,6 +18,8 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/collection/collection_grid.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/behaviour/route_tracker.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -43,7 +47,7 @@ class AvesApp extends StatefulWidget { _AvesAppState createState() => _AvesAppState(); } -class _AvesAppState extends State { +class _AvesAppState extends State with WidgetsBindingObserver { final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); late Future _appSetup; final _mediaStoreSource = MediaStoreSource(); @@ -70,6 +74,7 @@ class _AvesAppState extends State { _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)); _analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion()); _errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?)); + WidgetsBinding.instance!.addObserver(this); } @override @@ -158,35 +163,92 @@ class _AvesAppState extends State { ); } - Future _setup() async { - await settings.init( - monitorPlatformSettings: true, - isRotationLocked: await windowService.isRotationLocked(), - areAnimationsRemoved: await AccessibilityService.areAnimationsRemoved(), - ); - await device.init(); - FijkLog.setLevel(FijkLogLevel.Warn); - - // keep screen on - settings.updateStream.where((key) => key == Settings.keepScreenOnKey).listen( - (_) => settings.keepScreenOn.apply(), - ); - settings.keepScreenOn.apply(); - - // installed app access - settings.updateStream.where((key) => key == Settings.isInstalledAppAccessAllowedKey).listen( - (_) { - if (settings.isInstalledAppAccessAllowed) { - androidFileUtils.initAppNames(); - } else { - androidFileUtils.resetAppNames(); + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + debugPrint('$runtimeType lifecycle ${state.name}'); + switch (state) { + case AppLifecycleState.inactive: + switch (appModeNotifier.value) { + case AppMode.main: + case AppMode.pickMediaExternal: + _saveTopEntries(); + break; + case AppMode.pickMediaInternal: + case AppMode.pickFilterInternal: + case AppMode.view: + break; } - }, - ); + break; + case AppLifecycleState.paused: + case AppLifecycleState.detached: + case AppLifecycleState.resumed: + break; + } + } - // error reporting + // save IDs of entries visible at the top of the collection page with current layout settings + void _saveTopEntries() { + final stopwatch = Stopwatch()..start(); + final screenSize = window.physicalSize / window.devicePixelRatio; + var tileExtent = settings.getTileExtent(CollectionPage.routeName); + if (tileExtent == 0) { + tileExtent = screenSize.shortestSide / CollectionGrid.columnCountDefault; + } + final rows = (screenSize.height / tileExtent).ceil(); + final columns = (screenSize.width / tileExtent).ceil(); + final count = rows * columns; + final collection = CollectionLens(source: _mediaStoreSource, listenToSource: false); + settings.topEntryIds = collection.sortedEntries.take(count).map((entry) => entry.id).toList(); + collection.dispose(); + debugPrint('Saved $count top entries in ${stopwatch.elapsed.inMilliseconds}ms'); + } + + // setup before the first page is displayed. keep it short + Future _setup() async { + final stopwatch = Stopwatch()..start(); + + await device.init(); + await settings.init(monitorPlatformSettings: true); + settings.isRotationLocked = await windowService.isRotationLocked(); + settings.areAnimationsRemoved = await AccessibilityService.areAnimationsRemoved(); + _monitorSettings(); + + FijkLog.setLevel(FijkLogLevel.Warn); + unawaited(_setupErrorReporting()); + + debugPrint('App setup in ${stopwatch.elapsed.inMilliseconds}ms'); + } + + void _monitorSettings() { + void applyIsInstalledAppAccessAllowed() { + if (settings.isInstalledAppAccessAllowed) { + androidFileUtils.initAppNames(); + } else { + androidFileUtils.resetAppNames(); + } + } + + void applyKeepScreenOn() { + settings.keepScreenOn.apply(); + } + + void applyIsRotationLocked() { + if (!settings.isRotationLocked) { + windowService.requestOrientation(); + } + } + + settings.updateStream.where((event) => event.key == Settings.isInstalledAppAccessAllowedKey).listen((_) => applyIsInstalledAppAccessAllowed()); + settings.updateStream.where((event) => event.key == Settings.keepScreenOnKey).listen((_) => applyKeepScreenOn()); + settings.updateStream.where((event) => event.key == Settings.platformAccelerometerRotationKey).listen((_) => applyIsRotationLocked()); + + applyKeepScreenOn(); + applyIsRotationLocked(); + } + + Future _setupErrorReporting() async { await reportService.init(); - settings.updateStream.where((key) => key == Settings.isErrorReportingAllowedKey).listen( + settings.updateStream.where((event) => event.key == Settings.isErrorReportingAllowedKey).listen( (_) => reportService.setCollectionEnabled(settings.isErrorReportingAllowed), ); await reportService.setCollectionEnabled(settings.isErrorReportingAllowed); diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 7ba7144ff..bb810f95d 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/query.dart'; +import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/query.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; @@ -54,9 +56,13 @@ class _CollectionAppBarState extends State with SingleTickerPr CollectionLens get collection => widget.collection; + bool get isTrash => collection.filters.contains(TrashFilter.instance); + CollectionSource get source => collection.source; - bool get showFilterBar => collection.filters.any((v) => !(v is QueryFilter && v.live)); + Set get visibleFilters => collection.filters.where((v) => !(v is QueryFilter && v.live) && v is! TrashFilter).toSet(); + + bool get showFilterBar => visibleFilters.isNotEmpty; @override void initState() { @@ -110,36 +116,39 @@ class _CollectionAppBarState extends State with SingleTickerPr return AnimatedBuilder( animation: collection.filterChangeNotifier, builder: (context, child) { - final removableFilters = appMode != AppMode.pickInternal; + final removableFilters = appMode != AppMode.pickMediaInternal; return Selector( selector: (context, query) => query.enabled, builder: (context, queryEnabled, child) { - return SliverAppBar( - leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null, - title: SliverAppBarTitleWrapper( - child: _buildAppBarTitle(isSelecting), - ), - actions: _buildActions(selection), - bottom: PreferredSize( - preferredSize: Size.fromHeight(appBarBottomHeight), - child: Column( - children: [ - if (showFilterBar) - FilterBar( - filters: collection.filters.where((v) => !(v is QueryFilter && v.live)).toSet(), - removable: removableFilters, - onTap: removableFilters ? collection.removeFilter : null, - ), - if (queryEnabled) - EntryQueryBar( - queryNotifier: context.select>((query) => query.queryNotifier), - focusNode: _queryBarFocusNode, - ) - ], + return Selector>( + selector: (context, s) => s.collectionBrowsingQuickActions, + builder: (context, _, child) => SliverAppBar( + leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null, + title: SliverAppBarTitleWrapper( + child: _buildAppBarTitle(isSelecting), ), + actions: _buildActions(selection), + bottom: PreferredSize( + preferredSize: Size.fromHeight(appBarBottomHeight), + child: Column( + children: [ + if (showFilterBar) + FilterBar( + filters: visibleFilters, + removable: removableFilters, + onTap: removableFilters ? collection.removeFilter : null, + ), + if (queryEnabled) + EntryQueryBar( + queryNotifier: context.select>((query) => query.queryNotifier), + focusNode: _queryBarFocusNode, + ) + ], + ), + ), + titleSpacing: 0, + floating: true, ), - titleSpacing: 0, - floating: true, ); }, ); @@ -181,11 +190,11 @@ class _CollectionAppBarState extends State with SingleTickerPr if (isSelecting) { return Selector, int>( selector: (context, selection) => selection.selectedItems.length, - builder: (context, count, child) => Text(l10n.collectionSelectionPageTitle(count)), + builder: (context, count, child) => Text(count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count)), ); } else { final appMode = context.watch>().value; - Widget title = Text(appMode.isPicking ? l10n.collectionPickPageTitle : l10n.collectionPageTitle); + Widget title = Text(appMode.isPickingMedia ? l10n.collectionPickPageTitle : (isTrash ? l10n.binPageTitle : l10n.collectionPageTitle)); if (appMode == AppMode.main) { title = SourceStateAwareAppBarTitle( title: title, @@ -210,6 +219,7 @@ class _CollectionAppBarState extends State with SingleTickerPr isSelecting: isSelecting, itemCount: collection.entryCount, selectedItemCount: selectedItemCount, + isTrash: isTrash, ); bool canApply(EntrySetAction action) => _actionDelegate.canApply( action, @@ -220,7 +230,7 @@ class _CollectionAppBarState extends State with SingleTickerPr final canApplyEditActions = selectedItemCount > 0; final browsingQuickActions = settings.collectionBrowsingQuickActions; - final selectionQuickActions = settings.collectionSelectionQuickActions; + final selectionQuickActions = isTrash ? [EntrySetAction.delete, EntrySetAction.restore] : settings.collectionSelectionQuickActions; final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map( (action) => _toActionButton(action, enabled: canApply(action), selection: selection), ); @@ -236,13 +246,13 @@ class _CollectionAppBarState extends State with SingleTickerPr (action) => _toMenuItem(action, enabled: canApply(action), selection: selection), ); - final browsingMenuActions = EntrySetActions.browsing.where((v) => !browsingQuickActions.contains(v)); - final selectionMenuActions = EntrySetActions.selection.where((v) => !selectionQuickActions.contains(v)); + final browsingMenuActions = EntrySetActions.pageBrowsing.where((v) => !browsingQuickActions.contains(v)); + final selectionMenuActions = EntrySetActions.pageSelection.where((v) => !selectionQuickActions.contains(v)); final contextualMenuItems = [ ...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map( (action) => _toMenuItem(action, enabled: canApply(action), selection: selection), ), - if (isSelecting) + if (isSelecting && !isTrash) PopupMenuItem( enabled: canApplyEditActions, padding: EdgeInsets.zero, @@ -252,13 +262,7 @@ class _CollectionAppBarState extends State with SingleTickerPr title: context.l10n.collectionActionEdit, items: [ _buildRotateAndFlipMenuItems(context, canApply: canApply), - ...[ - EntrySetAction.editDate, - EntrySetAction.editLocation, - EntrySetAction.editRating, - EntrySetAction.editTags, - EntrySetAction.removeMetadata, - ].map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)), + ...EntrySetActions.edit.where(isVisible).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)), ], ), ), @@ -430,9 +434,11 @@ class _CollectionAppBarState extends State with SingleTickerPr case EntrySetAction.map: case EntrySetAction.stats: case EntrySetAction.rescan: + case EntrySetAction.emptyBin: // selecting case EntrySetAction.share: case EntrySetAction.delete: + case EntrySetAction.restore: case EntrySetAction.copy: case EntrySetAction.move: case EntrySetAction.toggleFavourite: diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 4041019b6..8f85f6995 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -37,11 +37,16 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class CollectionGrid extends StatefulWidget { - final String? settingsRouteKey; + final String settingsRouteKey; + + static const int columnCountDefault = 4; + static const double extentMin = 46; + static const double extentMax = 300; + static const double spacing = 2; const CollectionGrid({ Key? key, - this.settingsRouteKey, + required this.settingsRouteKey, }) : super(key: key); @override @@ -60,10 +65,11 @@ class _CollectionGridState extends State { @override Widget build(BuildContext context) { _tileExtentController ??= TileExtentController( - settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName!, - columnCountDefault: 4, - extentMin: 46, - spacing: 2, + settingsRouteKey: widget.settingsRouteKey, + columnCountDefault: CollectionGrid.columnCountDefault, + extentMin: CollectionGrid.extentMin, + extentMax: CollectionGrid.extentMax, + spacing: CollectionGrid.spacing, ); return TileExtentControllerProvider( controller: _tileExtentController!, @@ -90,35 +96,46 @@ class _CollectionGridContent extends StatelessWidget { final scrollableWidth = c.item1; final columnCount = c.item2; final tileSpacing = c.item3; - // do not listen for animation delay change - final target = context.read().staggeredAnimationPageTarget; - final tileAnimationDelay = context.read().getTileAnimationDelay(target); return GridTheme( extent: thumbnailExtent, child: EntryListDetailsTheme( extent: thumbnailExtent, - child: SectionedEntryListLayoutProvider( - collection: collection, - scrollableWidth: scrollableWidth, - tileLayout: tileLayout, - columnCount: columnCount, - spacing: tileSpacing, - tileExtent: thumbnailExtent, - tileBuilder: (entry) => AnimatedBuilder( - animation: favourites, - builder: (context, child) { - return InteractiveTile( - key: ValueKey(entry.contentId), - collection: collection, - entry: entry, - thumbnailExtent: thumbnailExtent, - tileLayout: tileLayout, - isScrollingNotifier: _isScrollingNotifier, - ); - }, - ), - tileAnimationDelay: tileAnimationDelay, - child: child!, + child: ValueListenableBuilder( + valueListenable: collection.source.stateNotifier, + builder: (context, sourceState, child) { + late final Duration tileAnimationDelay; + if (sourceState == SourceState.ready) { + // do not listen for animation delay change + final target = context.read().staggeredAnimationPageTarget; + tileAnimationDelay = context.read().getTileAnimationDelay(target); + } else { + tileAnimationDelay = Duration.zero; + } + return SectionedEntryListLayoutProvider( + collection: collection, + scrollableWidth: scrollableWidth, + tileLayout: tileLayout, + columnCount: columnCount, + spacing: tileSpacing, + tileExtent: thumbnailExtent, + tileBuilder: (entry) => AnimatedBuilder( + animation: favourites, + builder: (context, child) { + return InteractiveTile( + key: ValueKey(entry.id), + collection: collection, + entry: entry, + thumbnailExtent: thumbnailExtent, + tileLayout: tileLayout, + isScrollingNotifier: _isScrollingNotifier, + ); + }, + ), + tileAnimationDelay: tileAnimationDelay, + child: child!, + ); + }, + child: child, ), ), ); diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 94dcd625a..2f3b4cdca 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,6 +1,10 @@ +import 'dart:async'; + import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/query.dart'; +import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/common/basic/insets.dart'; @@ -29,10 +33,25 @@ class CollectionPage extends StatefulWidget { } class _CollectionPageState extends State { + final List _subscriptions = []; + CollectionLens get collection => widget.collection; + @override + void initState() { + super.initState(); + _subscriptions.add(settings.updateStream.where((event) => event.key == Settings.enableBinKey).listen((_) { + if (!settings.enableBin) { + collection.removeFilter(TrashFilter.instance); + } + })); + } + @override void dispose() { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); collection.dispose(); super.dispose(); } @@ -64,6 +83,7 @@ class _CollectionPageState extends State { child: const CollectionGrid( // key is expected by test driver key: Key('collection-grid'), + settingsRouteKey: CollectionPage.routeName, ), ), ), @@ -73,7 +93,7 @@ class _CollectionPageState extends State { ), ), ), - drawer: const AppDrawer(), + drawer: AppDrawer(currentCollection: collection), resizeToAvoidBottomInset: false, ), ); diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index db0e0d242..6c6f8766b 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_set_actions.dart'; @@ -8,29 +7,26 @@ import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/favourites.dart'; -import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/highlight.dart'; import 'package:aves/model/query.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; -import 'package:aves/services/media/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/mime_utils.dart'; -import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/entry_editor.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/filter_grids/album_pick.dart'; import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/stats/stats_page.dart'; @@ -39,13 +35,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { +import '../common/action_mixins/entry_storage.dart'; + +class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, EntryEditorMixin, EntryStorageMixin { bool isVisible( EntrySetAction action, { required AppMode appMode, required bool isSelecting, required int itemCount, required int selectedItemCount, + required bool isTrash, }) { switch (action) { // general @@ -63,15 +62,19 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa case EntrySetAction.toggleTitleSearch: return !isSelecting; case EntrySetAction.addShortcut: - return appMode == AppMode.main && !isSelecting && device.canPinShortcut; + return appMode == AppMode.main && !isSelecting && device.canPinShortcut && !isTrash; + case EntrySetAction.emptyBin: + return isTrash; // browsing or selecting case EntrySetAction.map: case EntrySetAction.stats: - case EntrySetAction.rescan: return appMode == AppMode.main; + case EntrySetAction.rescan: + return appMode == AppMode.main && !isTrash; // selecting - case EntrySetAction.share: case EntrySetAction.delete: + return appMode == AppMode.main && isSelecting; + case EntrySetAction.share: case EntrySetAction.copy: case EntrySetAction.move: case EntrySetAction.toggleFavourite: @@ -83,7 +86,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa case EntrySetAction.editRating: case EntrySetAction.editTags: case EntrySetAction.removeMetadata: - return appMode == AppMode.main && isSelecting; + return appMode == AppMode.main && isSelecting && !isTrash; + case EntrySetAction.restore: + return appMode == AppMode.main && isSelecting && isTrash; } } @@ -109,6 +114,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa case EntrySetAction.toggleTitleSearch: case EntrySetAction.addShortcut: return true; + case EntrySetAction.emptyBin: + return !isSelecting && hasItems; case EntrySetAction.map: case EntrySetAction.stats: case EntrySetAction.rescan: @@ -116,6 +123,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa // selecting case EntrySetAction.share: case EntrySetAction.delete: + case EntrySetAction.restore: case EntrySetAction.copy: case EntrySetAction.move: case EntrySetAction.toggleFavourite: @@ -164,8 +172,12 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa _share(context); break; case EntrySetAction.delete: + case EntrySetAction.emptyBin: _delete(context); break; + case EntrySetAction.restore: + _move(context, moveType: MoveType.fromBin); + break; case EntrySetAction.copy: _move(context, moveType: MoveType.copy); break; @@ -202,83 +214,77 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa } } - Set _getExpandedSelectedItems(Selection selection) { - return selection.selectedItems.expand((entry) => entry.burstEntries ?? {entry}).toSet(); + Set _getTargetItems(BuildContext context) { + final selection = context.read>(); + final groupedEntries = (selection.isSelecting ? selection.selectedItems : context.read().sortedEntries); + return groupedEntries.expand((entry) => entry.burstEntries ?? {entry}).toSet(); } void _share(BuildContext context) { - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - androidAppService.shareEntries(selectedItems).then((success) { + final entries = _getTargetItems(context); + androidAppService.shareEntries(entries).then((success) { if (!success) showNoMatchingAppDialog(context); }); } void _rescan(BuildContext context) { - final selection = context.read>(); - final collection = context.read(); - final entries = (selection.isSelecting ? _getExpandedSelectedItems(selection) : collection.sortedEntries.toSet()); + final entries = _getTargetItems(context); final controller = AnalysisController(canStartService: true, force: true); + final collection = context.read(); collection.source.analyze(controller, entries: entries); + final selection = context.read>(); selection.browse(); } Future _toggleFavourite(BuildContext context) async { - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - if (selectedItems.every((entry) => entry.isFavourite)) { - await favourites.remove(selectedItems); + final entries = _getTargetItems(context); + if (entries.every((entry) => entry.isFavourite)) { + await favourites.removeEntries(entries); } else { - await favourites.add(selectedItems); + await favourites.add(entries); } + final selection = context.read>(); selection.browse(); } Future _delete(BuildContext context) async { + final entries = _getTargetItems(context); + + final pureTrash = entries.every((entry) => entry.trashed); + if (settings.enableBin && !pureTrash) { + await _move(context, moveType: MoveType.toBin); + return; + } + + final l10n = context.l10n; final source = context.read(); - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet(); - final todoCount = selectedItems.length; + final selectionDirs = entries.map((e) => e.directory).whereNotNull().toSet(); + final todoCount = entries.length; - final confirmed = await showDialog( + if (!(await showConfirmationDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.deleteButtonLabel), - ), - ], - ); - }, - ); - if (confirmed == null || !confirmed) return; + type: ConfirmationDialog.delete, + message: l10n.deleteEntriesConfirmationDialogMessage(todoCount), + confirmationButtonLabel: l10n.deleteButtonLabel, + ))) return; - if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return; + if (!pureTrash && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: entries)) return; source.pauseMonitoring(); final opId = mediaFileService.newOpId; - showOpReport( + await showOpReport( context: context, - opStream: mediaFileService.delete(opId: opId, entries: selectedItems), + opStream: mediaFileService.delete(opId: opId, entries: entries), itemCount: todoCount, onCancel: () => mediaFileService.cancelFileOp(opId), onDone: (processed) async { final successOps = processed.where((e) => e.success).toSet(); final deletedOps = successOps.where((e) => !e.skipped).toSet(); final deletedUris = deletedOps.map((event) => event.uri).toSet(); - await source.removeEntries(deletedUris); - selection.browse(); + await source.removeEntries(deletedUris, includeTrash: true); source.resumeMonitoring(); final successCount = successOps.length; @@ -291,140 +297,21 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa await storageService.deleteEmptyDirectories(selectionDirs); }, ); + + final selection = context.read>(); + selection.browse(); } Future _move(BuildContext context, {required MoveType moveType}) async { - final l10n = context.l10n; + final entries = _getTargetItems(context); + await move(context, moveType: moveType, entries: entries); + final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet(); - - final destinationAlbum = await pickAlbum(context: context, moveType: moveType); - if (destinationAlbum == null) return; - if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; - - if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return; - - if (!await checkFreeSpaceForMove(context, selectedItems, destinationAlbum, moveType)) return; - - // do not directly use selection when moving and post-processing items - // as source monitoring may remove obsolete items from the original selection - final todoItems = selectedItems.toSet(); - - final copy = moveType == MoveType.copy; - final todoCount = todoItems.length; - assert(todoCount > 0); - - final destinationDirectory = Directory(destinationAlbum); - final names = [ - ...todoItems.map((v) => '${v.filenameWithoutExtension}${v.extension}'), - // do not guard up front based on directory existence, - // as conflicts could be within moved entries scattered across multiple albums - if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)), - ]; - final uniqueNames = names.toSet(); - var nameConflictStrategy = NameConflictStrategy.rename; - if (uniqueNames.length < names.length) { - final value = await showDialog( - context: context, - builder: (context) { - return AvesSelectionDialog( - initialValue: nameConflictStrategy, - options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))), - message: selectionDirs.length == 1 ? l10n.nameConflictDialogSingleSourceMessage : l10n.nameConflictDialogMultipleSourceMessage, - confirmationButtonLabel: l10n.continueButtonLabel, - ); - }, - ); - if (value == null) return; - nameConflictStrategy = value; - } - - final source = context.read(); - source.pauseMonitoring(); - final opId = mediaFileService.newOpId; - showOpReport( - context: context, - opStream: mediaFileService.move( - opId: opId, - entries: todoItems, - copy: copy, - destinationAlbum: destinationAlbum, - nameConflictStrategy: nameConflictStrategy, - ), - itemCount: todoCount, - onCancel: () => mediaFileService.cancelFileOp(opId), - onDone: (processed) async { - final successOps = processed.where((e) => e.success).toSet(); - final movedOps = successOps.where((e) => !e.skipped).toSet(); - await source.updateAfterMove( - todoEntries: todoItems, - copy: copy, - destinationAlbum: destinationAlbum, - movedOps: movedOps, - ); - selection.browse(); - source.resumeMonitoring(); - - // cleanup - if (moveType == MoveType.move) { - await storageService.deleteEmptyDirectories(selectionDirs); - } - - final successCount = successOps.length; - if (successCount < todoCount) { - final count = todoCount - successCount; - showFeedback(context, copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count)); - } else { - final count = movedOps.length; - showFeedback( - context, - copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count), - count > 0 - ? SnackBarAction( - label: l10n.showButtonLabel, - onPressed: () async { - final highlightInfo = context.read(); - final collection = context.read(); - var targetCollection = collection; - if (collection.filters.any((f) => f is AlbumFilter)) { - final filter = AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum)); - // we could simply add the filter to the current collection - // but navigating makes the change less jarring - targetCollection = CollectionLens( - source: collection.source, - filters: collection.filters, - )..addFilter(filter); - unawaited(Navigator.pushReplacement( - context, - MaterialPageRoute( - settings: const RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage( - collection: targetCollection, - ), - ), - )); - final delayDuration = context.read().staggeredAnimationPageTarget; - await Future.delayed(delayDuration); - } - await Future.delayed(Durations.highlightScrollInitDelay); - final newUris = movedOps.map((v) => v.newFields['uri'] as String?).toSet(); - final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri)); - if (targetEntry != null) { - highlightInfo.trackItem(targetEntry, highlightItem: targetEntry); - } - }, - ) - : null, - ); - } - }, - ); + selection.browse(); } Future _edit( BuildContext context, - Selection selection, Set todoItems, Future> Function(AvesEntry entry) op, ) async { @@ -439,7 +326,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa final source = context.read(); source.pauseMonitoring(); var cancelled = false; - showOpReport( + await showOpReport( context: context, opStream: Stream.fromIterable(todoItems).asyncMap((entry) async { if (cancelled) { @@ -454,7 +341,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa onDone: (processed) async { final successOps = processed.where((e) => e.success).toSet(); final editedOps = successOps.where((e) => !e.skipped).toSet(); - selection.browse(); source.resumeMonitoring(); unawaited(source.refreshUris(editedOps.map((v) => v.uri).toSet()).then((_) { @@ -480,14 +366,15 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa } }, ); + final selection = context.read>(); + selection.browse(); } - Future?> _getEditableItems( + Future?> _getEditableTargetItems( BuildContext context, { - required Set selectedItems, required bool Function(AvesEntry entry) canEdit, }) async { - final bySupported = groupBy(selectedItems, canEdit); + final bySupported = groupBy(_getTargetItems(context), canEdit); final supported = (bySupported[true] ?? []).toSet(); final unsupported = (bySupported[false] ?? []).toSet(); @@ -523,70 +410,52 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa } Future _rotate(BuildContext context, {required bool clockwise}) async { - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - - final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRotateAndFlip); + final todoItems = await _getEditableTargetItems(context, canEdit: (entry) => entry.canRotateAndFlip); if (todoItems == null || todoItems.isEmpty) return; - await _edit(context, selection, todoItems, (entry) => entry.rotate(clockwise: clockwise)); + await _edit(context, todoItems, (entry) => entry.rotate(clockwise: clockwise)); } Future _flip(BuildContext context) async { - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - - final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRotateAndFlip); + final todoItems = await _getEditableTargetItems(context, canEdit: (entry) => entry.canRotateAndFlip); if (todoItems == null || todoItems.isEmpty) return; - await _edit(context, selection, todoItems, (entry) => entry.flip()); + await _edit(context, todoItems, (entry) => entry.flip()); } Future _editDate(BuildContext context) async { - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - - final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canEditDate); + final todoItems = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditDate); if (todoItems == null || todoItems.isEmpty) return; final modifier = await selectDateModifier(context, todoItems); if (modifier == null) return; - await _edit(context, selection, todoItems, (entry) => entry.editDate(modifier)); + await _edit(context, todoItems, (entry) => entry.editDate(modifier)); } Future _editLocation(BuildContext context) async { - final collection = context.read(); - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - - final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canEditLocation); + final todoItems = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditLocation); if (todoItems == null || todoItems.isEmpty) return; + final collection = context.read(); final location = await selectLocation(context, todoItems, collection); if (location == null) return; - await _edit(context, selection, todoItems, (entry) => entry.editLocation(location)); + await _edit(context, todoItems, (entry) => entry.editLocation(location)); } Future _editRating(BuildContext context) async { - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - - final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canEditRating); + final todoItems = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditRating); if (todoItems == null || todoItems.isEmpty) return; final rating = await selectRating(context, todoItems); if (rating == null) return; - await _edit(context, selection, todoItems, (entry) => entry.editRating(rating)); + await _edit(context, todoItems, (entry) => entry.editRating(rating)); } Future _editTags(BuildContext context) async { - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - - final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canEditTags); + final todoItems = await _getEditableTargetItems(context, canEdit: (entry) => entry.canEditTags); if (todoItems == null || todoItems.isEmpty) return; final newTagsByEntry = await selectTags(context, todoItems); @@ -601,26 +470,22 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa if (todoItems.isEmpty) return; - await _edit(context, selection, todoItems, (entry) => entry.editTags(newTagsByEntry[entry]!)); + await _edit(context, todoItems, (entry) => entry.editTags(newTagsByEntry[entry]!)); } Future _removeMetadata(BuildContext context) async { - final selection = context.read>(); - final selectedItems = _getExpandedSelectedItems(selection); - - final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canRemoveMetadata); + final todoItems = await _getEditableTargetItems(context, canEdit: (entry) => entry.canRemoveMetadata); if (todoItems == null || todoItems.isEmpty) return; final types = await selectMetadataToRemove(context, todoItems); if (types == null || types.isEmpty) return; - await _edit(context, selection, todoItems, (entry) => entry.removeMetadata(types)); + await _edit(context, todoItems, (entry) => entry.removeMetadata(types)); } void _goToMap(BuildContext context) { - final selection = context.read>(); final collection = context.read(); - final entries = (selection.isSelecting ? _getExpandedSelectedItems(selection) : collection.sortedEntries); + final entries = _getTargetItems(context); Navigator.push( context, @@ -639,9 +504,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa } void _goToStats(BuildContext context) { - final selection = context.read>(); final collection = context.read(); - final entries = selection.isSelecting ? _getExpandedSelectedItems(selection) : collection.sortedEntries.toSet(); + final entries = _getTargetItems(context); Navigator.push( context, diff --git a/lib/widgets/collection/grid/list_details.dart b/lib/widgets/collection/grid/list_details.dart index 5170b5327..98d2a6636 100644 --- a/lib/widgets/collection/grid/list_details.dart +++ b/lib/widgets/collection/grid/list_details.dart @@ -1,5 +1,5 @@ import 'package:aves/model/entry.dart'; -import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index 0caf3dfed..9b02c1813 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -43,12 +43,13 @@ class InteractiveTile extends StatelessWidget { _goToViewer(context); } break; - case AppMode.pickExternal: + case AppMode.pickMediaExternal: ViewerService.pick(entry.uri); break; - case AppMode.pickInternal: + case AppMode.pickMediaInternal: Navigator.pop(context, entry); break; + case AppMode.pickFilterInternal: case AppMode.view: break; } @@ -65,7 +66,7 @@ class InteractiveTile extends StatelessWidget { // hero tag should include a collection identifier, so that it animates // between different views of the entry in the same collection (e.g. thumbnails <-> viewer) // but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer) - heroTagger: () => Object.hashAll([collection.id, entry.uri]), + heroTagger: () => Object.hashAll([collection.id, entry.id]), ), ), ); @@ -80,7 +81,7 @@ class InteractiveTile extends StatelessWidget { final viewerCollection = collection.copyWith( listenToSource: false, ); - assert(viewerCollection.sortedEntries.map((e) => e.contentId).contains(entry.contentId)); + assert(viewerCollection.sortedEntries.map((entry) => entry.id).contains(entry.id)); return EntryViewerPage( collection: viewerCollection, initialEntry: entry, diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart new file mode 100644 index 000000000..4c458f894 --- /dev/null +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -0,0 +1,211 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:aves/app_mode.dart'; +import 'package:aves/model/actions/move_type.dart'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/trash.dart'; +import 'package:aves/model/highlight.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/services/common/image_op_events.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/services/media/enums.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; +import 'package:aves/widgets/common/action_mixins/size_aware.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { + Future move( + BuildContext context, { + required MoveType moveType, + required Set entries, + VoidCallback? onSuccess, + }) async { + final todoCount = entries.length; + assert(todoCount > 0); + + final toBin = moveType == MoveType.toBin; + final copy = moveType == MoveType.copy; + + final l10n = context.l10n; + if (toBin) { + if (!(await showConfirmationDialog( + context: context, + type: ConfirmationDialog.moveToBin, + message: l10n.binEntriesConfirmationDialogMessage(todoCount), + confirmationButtonLabel: l10n.deleteButtonLabel, + ))) return; + } + + final source = context.read(); + if (source.initState != SourceInitializationState.full) { + // source may be uninitialized in viewer mode + await source.init(); + } + + final entriesByDestination = >{}; + switch (moveType) { + case MoveType.copy: + case MoveType.move: + case MoveType.export: + final destinationAlbum = await pickAlbum(context: context, moveType: moveType); + if (destinationAlbum == null) return; + entriesByDestination[destinationAlbum] = entries; + break; + case MoveType.toBin: + entriesByDestination[AndroidFileUtils.trashDirPath] = entries; + break; + case MoveType.fromBin: + groupBy(entries, (e) => e.directory).forEach((originAlbum, dirEntries) { + if (originAlbum != null) { + entriesByDestination[originAlbum] = dirEntries.toSet(); + } + }); + break; + } + + // permission for modification at destinations + final destinationAlbums = entriesByDestination.keys.toSet(); + if (!await checkStoragePermissionForAlbums(context, destinationAlbums)) return; + + // permission for modification at origins + final originAlbums = entries.map((e) => e.directory).whereNotNull().toSet(); + if ({MoveType.move, MoveType.toBin}.contains(moveType) && !await checkStoragePermissionForAlbums(context, originAlbums, entries: entries)) return; + + await Future.forEach(destinationAlbums, (destinationAlbum) async { + if (!await checkFreeSpaceForMove(context, entries, destinationAlbum, moveType)) return; + }); + + var nameConflictStrategy = NameConflictStrategy.rename; + if (!toBin && destinationAlbums.length == 1) { + final destinationDirectory = Directory(destinationAlbums.single); + final names = [ + ...entries.map((v) => '${v.filenameWithoutExtension}${v.extension}'), + // do not guard up front based on directory existence, + // as conflicts could be within moved entries scattered across multiple albums + if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)), + ]; + final uniqueNames = names.toSet(); + if (uniqueNames.length < names.length) { + final value = await showDialog( + context: context, + builder: (context) { + return AvesSelectionDialog( + initialValue: nameConflictStrategy, + options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))), + message: originAlbums.length == 1 ? l10n.nameConflictDialogSingleSourceMessage : l10n.nameConflictDialogMultipleSourceMessage, + confirmationButtonLabel: l10n.continueButtonLabel, + ); + }, + ); + if (value == null) return; + nameConflictStrategy = value; + } + } + + source.pauseMonitoring(); + final opId = mediaFileService.newOpId; + await showOpReport( + context: context, + opStream: mediaFileService.move( + opId: opId, + entriesByDestination: entriesByDestination, + copy: copy, + nameConflictStrategy: nameConflictStrategy, + ), + itemCount: todoCount, + onCancel: () => mediaFileService.cancelFileOp(opId), + onDone: (processed) async { + final successOps = processed.where((e) => e.success).toSet(); + final movedOps = successOps.where((e) => !e.skipped).toSet(); + await source.updateAfterMove( + todoEntries: entries, + moveType: moveType, + destinationAlbums: destinationAlbums, + movedOps: movedOps, + ); + source.resumeMonitoring(); + + // cleanup + if ({MoveType.move, MoveType.toBin}.contains(moveType)) { + await storageService.deleteEmptyDirectories(originAlbums); + } + + final successCount = successOps.length; + if (successCount < todoCount) { + final count = todoCount - successCount; + showFeedback(context, copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count)); + } else { + final count = movedOps.length; + final appMode = context.read>().value; + + SnackBarAction? action; + if (count > 0 && appMode == AppMode.main && !toBin) { + action = SnackBarAction( + label: l10n.showButtonLabel, + onPressed: () async { + late CollectionLens targetCollection; + + final highlightInfo = context.read(); + final collection = context.read(); + if (collection != null) { + targetCollection = collection; + } + if (collection == null || collection.filters.any((f) => f is AlbumFilter || f is TrashFilter)) { + targetCollection = CollectionLens( + source: source, + filters: collection?.filters.where((f) => f != TrashFilter.instance).toSet(), + ); + // we could simply add the filter to the current collection + // but navigating makes the change less jarring + if (destinationAlbums.length == 1) { + final destinationAlbum = destinationAlbums.single; + final filter = AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum)); + targetCollection.addFilter(filter); + } + unawaited(Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: const RouteSettings(name: CollectionPage.routeName), + builder: (context) => CollectionPage( + collection: targetCollection, + ), + ), + (route) => false, + )); + final delayDuration = context.read().staggeredAnimationPageTarget; + await Future.delayed(delayDuration); + } + await Future.delayed(Durations.highlightScrollInitDelay); + final newUris = movedOps.map((v) => v.newFields['uri'] as String?).toSet(); + final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri)); + if (targetEntry != null) { + highlightInfo.trackItem(targetEntry, highlightItem: targetEntry); + } + }, + ); + } + showFeedback( + context, + copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count), + action, + ); + onSuccess?.call(); + } + }, + ); + } +} diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 729c83fd2..b3ab4343d 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/model/settings/accessibility_animations.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/theme/durations.dart'; @@ -69,14 +69,14 @@ mixin FeedbackMixin { // report overlay for multiple operations - void showOpReport({ + Future showOpReport({ required BuildContext context, required Stream opStream, required int itemCount, VoidCallback? onCancel, void Function(Set processed)? onDone, }) { - showDialog( + return showDialog( context: context, barrierDismissible: false, builder: (context) => ReportOverlay( @@ -118,7 +118,7 @@ class _ReportOverlayState extends State> with SingleTickerPr Stream get opStream => widget.opStream; static const fontSize = 18.0; - static const radius = 160.0; + static const diameter = 160.0; static const strokeWidth = 8.0; @override @@ -169,8 +169,8 @@ class _ReportOverlayState extends State> with SingleTickerPr alignment: Alignment.center, children: [ Container( - width: radius + 2, - height: radius + 2, + width: diameter + 2, + height: diameter + 2, decoration: const BoxDecoration( color: Color(0xBB000000), shape: BoxShape.circle, @@ -178,8 +178,8 @@ class _ReportOverlayState extends State> with SingleTickerPr ), if (animate) Container( - width: radius, - height: radius, + width: diameter, + height: diameter, padding: const EdgeInsets.all(strokeWidth / 2), child: CircularProgressIndicator( color: progressColor.withOpacity(.1), @@ -189,7 +189,7 @@ class _ReportOverlayState extends State> with SingleTickerPr CircularPercentIndicator( percent: percent, lineWidth: strokeWidth, - radius: radius, + radius: diameter / 2, backgroundColor: Colors.white24, progressColor: progressColor, animation: animate, @@ -203,8 +203,8 @@ class _ReportOverlayState extends State> with SingleTickerPr Material( color: Colors.transparent, child: Container( - width: radius, - height: radius, + width: diameter, + height: diameter, margin: const EdgeInsets.only(top: fontSize), alignment: const FractionalOffset(0.5, 0.75), child: Tooltip( @@ -279,7 +279,7 @@ class _FeedbackMessageState extends State<_FeedbackMessage> { CircularPercentIndicator( percent: _percent, lineWidth: 2, - radius: 32, + radius: 16, // progress color is provided by the caller, // because we cannot use the app context theme here backgroundColor: widget.progressColor, diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index a7f37b123..bb6a983c4 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -19,6 +19,8 @@ mixin SizeAwareMixin { String destinationAlbum, MoveType moveType, ) async { + if (moveType == MoveType.toBin) return true; + // assume we have enough space if we cannot find the volume or its remaining free space final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); if (destinationVolume == null) return true; @@ -27,13 +29,15 @@ mixin SizeAwareMixin { if (free == null) return true; late int needed; - int sumSize(sum, entry) => sum + entry.sizeBytes ?? 0; + int sumSize(sum, entry) => sum + (entry.sizeBytes ?? 0); switch (moveType) { case MoveType.copy: case MoveType.export: needed = selection.fold(0, sumSize); break; case MoveType.move: + case MoveType.toBin: + case MoveType.fromBin: // 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)).whereNotNullKey(); final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); diff --git a/lib/widgets/common/basic/menu.dart b/lib/widgets/common/basic/menu.dart index 58a5dcc4f..8bb4db14a 100644 --- a/lib/widgets/common/basic/menu.dart +++ b/lib/widgets/common/basic/menu.dart @@ -52,7 +52,7 @@ class PopupMenuItemExpansionPanel extends StatefulWidget { final bool enabled; final IconData icon; final String title; - final List> items; + final List> items; const PopupMenuItemExpansionPanel({ Key? key, diff --git a/lib/widgets/common/favourite_toggler.dart b/lib/widgets/common/favourite_toggler.dart index caec381e2..319c1d8ac 100644 --- a/lib/widgets/common/favourite_toggler.dart +++ b/lib/widgets/common/favourite_toggler.dart @@ -1,5 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -72,7 +73,7 @@ class _FavouriteTogglerState extends State { ), Sweeper( key: ValueKey(entries.length == 1 ? entries.first : entries.length), - builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent), + builder: (context) => const Icon(AIcons.favourite, color: AColors.favourite), toggledNotifier: isFavouriteNotifier, ), ], diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index f7b16f219..3bb375a9e 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -148,6 +148,8 @@ class _GridItemTrackerState extends State> with WidgetsBin } void _onLayoutChange() { + if (scrollController.positions.length != 1) return; + // do not track when view shows top edge if (scrollController.offset == 0) return; diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart index 2422ab2bd..670c9995e 100644 --- a/lib/widgets/common/grid/theme.dart +++ b/lib/widgets/common/grid/theme.dart @@ -6,13 +6,14 @@ import 'package:provider/provider.dart'; class GridTheme extends StatelessWidget { final double extent; - final bool? showLocation; + final bool? showLocation, showTrash; final Widget child; const GridTheme({ Key? key, required this.extent, this.showLocation, + this.showTrash, required this.child, }) : super(key: key); @@ -33,6 +34,7 @@ class GridTheme extends StatelessWidget { showMotionPhoto: settings.showThumbnailMotionPhoto, showRating: settings.showThumbnailRating, showRaw: settings.showThumbnailRaw, + showTrash: showTrash ?? true, showVideoDuration: settings.showThumbnailVideoDuration, ); }, @@ -43,7 +45,7 @@ class GridTheme extends StatelessWidget { class GridThemeData { final double iconSize, fontSize, highlightBorderWidth; - final bool showFavourite, showLocation, showMotionPhoto, showRating, showRaw, showVideoDuration; + final bool showFavourite, showLocation, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration; const GridThemeData({ required this.iconSize, @@ -54,6 +56,7 @@ class GridThemeData { required this.showMotionPhoto, required this.showRating, required this.showRaw, + required this.showTrash, required this.showVideoDuration, }); } diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index b8504df7d..7061b9f99 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -4,7 +4,7 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/settings/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index efd6550f7..f259495e2 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -2,6 +2,7 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -177,6 +178,31 @@ class RatingIcon extends StatelessWidget { } } +class TrashIcon extends StatelessWidget { + final int? trashDaysLeft; + + const TrashIcon({ + Key? key, + required this.trashDaysLeft, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final child = OverlayIcon( + icon: AIcons.bin, + text: trashDaysLeft != null ? context.l10n.timeDays(trashDaysLeft!) : null, + ); + + return DefaultTextStyle( + style: TextStyle( + color: Colors.grey.shade200, + fontSize: context.select((t) => t.fontSize), + ), + child: child, + ); + } +} + class OverlayIcon extends StatelessWidget { final IconData icon; final String? text; diff --git a/lib/widgets/common/identity/empty.dart b/lib/widgets/common/identity/empty.dart index 459726dbc..af4ff6096 100644 --- a/lib/widgets/common/identity/empty.dart +++ b/lib/widgets/common/identity/empty.dart @@ -7,6 +7,7 @@ class EmptyContent extends StatelessWidget { final String text; final AlignmentGeometry alignment; final double fontSize; + final bool safeBottom; const EmptyContent({ Key? key, @@ -14,15 +15,18 @@ class EmptyContent extends StatelessWidget { required this.text, this.alignment = const FractionalOffset(.5, .35), this.fontSize = 22, + this.safeBottom = true, }) : super(key: key); @override Widget build(BuildContext context) { const color = Colors.blueGrey; return Padding( - padding: EdgeInsets.only( - bottom: context.select((mq) => mq.effectiveBottomPadding), - ), + padding: safeBottom + ? EdgeInsets.only( + bottom: context.select((mq) => mq.effectiveBottomPadding), + ) + : EdgeInsets.zero, child: Align( alignment: alignment, child: Column( diff --git a/lib/widgets/common/magnifier/controller/controller.dart b/lib/widgets/common/magnifier/controller/controller.dart index 0e9d41e15..cdec420f7 100644 --- a/lib/widgets/common/magnifier/controller/controller.dart +++ b/lib/widgets/common/magnifier/controller/controller.dart @@ -71,6 +71,7 @@ class MagnifierController { position = position ?? this.position; scale = scale ?? this.scale; if (this.position == position && this.scale == scale) return; + assert((scale ?? 0) >= 0); previousState = currentState; _setState(MagnifierState( @@ -127,7 +128,7 @@ class MagnifierController { case ScaleState.covering: return _clamp(ScaleLevel.scaleForCovering(scaleBoundaries.viewportSize, scaleBoundaries.childSize)); case ScaleState.originalSize: - return _clamp(1.0); + return _clamp(scaleBoundaries.originalScale); default: return null; } diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index 214cb5089..da4ad3c5e 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:aves/widgets/common/magnifier/controller/controller.dart'; import 'package:aves/widgets/common/magnifier/controller/controller_delegate.dart'; import 'package:aves/widgets/common/magnifier/controller/state.dart'; @@ -132,7 +134,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM updateScaleStateFromNewScale(newScale, ChangeSource.gesture); updateMultiple( - scale: newScale, + scale: max(0, newScale), position: newPosition, source: ChangeSource.gesture, ); diff --git a/lib/widgets/common/magnifier/scale/scale_boundaries.dart b/lib/widgets/common/magnifier/scale/scale_boundaries.dart index 85e54b0a8..986fc82ec 100644 --- a/lib/widgets/common/magnifier/scale/scale_boundaries.dart +++ b/lib/widgets/common/magnifier/scale/scale_boundaries.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:ui'; import 'package:aves/widgets/common/magnifier/controller/controller.dart'; @@ -41,11 +42,13 @@ class ScaleBoundaries extends Equatable { } } - double get minScale => _scaleForLevel(_minScale); + double get originalScale => 1.0 / window.devicePixelRatio; - double get maxScale => _scaleForLevel(_maxScale).clamp(minScale, double.infinity); + double get minScale => {_scaleForLevel(_minScale), originalScale, initialScale}.fold(double.infinity, min); - double get initialScale => _scaleForLevel(_initialScale).clamp(minScale, maxScale); + double get maxScale => {_scaleForLevel(_maxScale), originalScale, initialScale}.fold(0, max); + + double get initialScale => _scaleForLevel(_initialScale); Offset get _viewportCenter => viewportSize.center(Offset.zero); diff --git a/lib/widgets/common/map/attribution.dart b/lib/widgets/common/map/attribution.dart index 030c68caf..afeb94a2b 100644 --- a/lib/widgets/common/map/attribution.dart +++ b/lib/widgets/common/map/attribution.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/map/buttons.dart b/lib/widgets/common/map/buttons.dart index 0a933970d..48223c7cb 100644 --- a/lib/widgets/common/map/buttons.dart +++ b/lib/widgets/common/map/buttons.dart @@ -1,6 +1,6 @@ import 'package:aves/model/filters/coordinate.dart'; -import 'package:aves/model/settings/enums.dart'; -import 'package:aves/model/settings/map_style.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index f52cae77e..b9862fdee 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/settings/enums.dart'; -import 'package:aves/model/settings/map_style.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/constants.dart'; @@ -152,117 +151,111 @@ class _GeoMapState extends State { onTap(clusterAverageLocation, markerEntry, getClusterEntries); } - return FutureBuilder( - future: availability.isConnected, - builder: (context, snapshot) { - if (snapshot.data != true) return const SizedBox(); - return Selector( - selector: (context, s) => s.infoMapStyle, - builder: (context, mapStyle, child) { - final isGoogleMaps = mapStyle.isGoogleMaps; - final progressive = !isGoogleMaps; - Widget _buildMarkerWidget(MarkerKey key) => ImageMarker( - key: key, - entry: key.entry, - count: key.count, - extent: GeoMap.markerImageExtent, - arrowSize: GeoMap.markerArrowSize, - progressive: progressive, - ); + return Selector( + selector: (context, s) => s.infoMapStyle, + builder: (context, mapStyle, child) { + final isGoogleMaps = mapStyle.isGoogleMaps; + final progressive = !isGoogleMaps; + Widget _buildMarkerWidget(MarkerKey key) => ImageMarker( + key: key, + entry: key.entry, + count: key.count, + extent: GeoMap.markerImageExtent, + arrowSize: GeoMap.markerArrowSize, + progressive: progressive, + ); - Widget child = isGoogleMaps - ? EntryGoogleMap( - controller: widget.controller, - clusterListenable: _clusterChangeNotifier, - boundsNotifier: _boundsNotifier, - minZoom: 0, - maxZoom: 20, - style: mapStyle, - markerClusterBuilder: _buildMarkerClusters, - markerWidgetBuilder: _buildMarkerWidget, - dotLocationNotifier: widget.dotLocationNotifier, - onUserZoomChange: widget.onUserZoomChange, - onMapTap: widget.onMapTap, - onMarkerTap: _onMarkerTap, - openMapPage: widget.openMapPage, - ) - : EntryLeafletMap( - controller: widget.controller, - clusterListenable: _clusterChangeNotifier, - boundsNotifier: _boundsNotifier, - minZoom: 2, - maxZoom: 16, - style: mapStyle, - markerClusterBuilder: _buildMarkerClusters, - markerWidgetBuilder: _buildMarkerWidget, - dotLocationNotifier: widget.dotLocationNotifier, - markerSize: Size( - GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2, - GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.markerArrowSize.height, - ), - dotMarkerSize: const Size( - DotMarker.diameter + ImageMarker.outerBorderWidth * 2, - DotMarker.diameter + ImageMarker.outerBorderWidth * 2, - ), - onUserZoomChange: widget.onUserZoomChange, - onMapTap: widget.onMapTap, - onMarkerTap: _onMarkerTap, - openMapPage: widget.openMapPage, - ); - - final mapHeight = context.select((v) => v.mapHeight); - child = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - mapHeight != null - ? SizedBox( - height: mapHeight, - child: child, - ) - : Expanded(child: child), - SafeArea( - top: false, - bottom: false, - child: Attribution(style: mapStyle), + Widget child = isGoogleMaps + ? EntryGoogleMap( + controller: widget.controller, + clusterListenable: _clusterChangeNotifier, + boundsNotifier: _boundsNotifier, + minZoom: 0, + maxZoom: 20, + style: mapStyle, + markerClusterBuilder: _buildMarkerClusters, + markerWidgetBuilder: _buildMarkerWidget, + dotLocationNotifier: widget.dotLocationNotifier, + onUserZoomChange: widget.onUserZoomChange, + onMapTap: widget.onMapTap, + onMarkerTap: _onMarkerTap, + openMapPage: widget.openMapPage, + ) + : EntryLeafletMap( + controller: widget.controller, + clusterListenable: _clusterChangeNotifier, + boundsNotifier: _boundsNotifier, + minZoom: 2, + maxZoom: 16, + style: mapStyle, + markerClusterBuilder: _buildMarkerClusters, + markerWidgetBuilder: _buildMarkerWidget, + dotLocationNotifier: widget.dotLocationNotifier, + markerSize: Size( + GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2, + GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.markerArrowSize.height, ), - ], - ); + dotMarkerSize: const Size( + DotMarker.diameter + ImageMarker.outerBorderWidth * 2, + DotMarker.diameter + ImageMarker.outerBorderWidth * 2, + ), + onUserZoomChange: widget.onUserZoomChange, + onMapTap: widget.onMapTap, + onMarkerTap: _onMarkerTap, + openMapPage: widget.openMapPage, + ); - return AnimatedSize( - alignment: Alignment.topCenter, - curve: Curves.easeInOutCubic, - duration: Durations.mapStyleSwitchAnimation, - child: ValueListenableBuilder( - valueListenable: widget.isAnimatingNotifier, - builder: (context, animating, child) { - if (!animating && isGoogleMaps) { - _googleMapsLoaded = true; - } - Widget replacement = Stack( - children: [ - const MapDecorator(), - MapButtonPanel( - boundsNotifier: _boundsNotifier, - openMapPage: widget.openMapPage, - ), - ], - ); - if (mapHeight != null) { - replacement = SizedBox( - height: mapHeight, - child: replacement, - ); - } - return Visibility( - visible: !isGoogleMaps || _googleMapsLoaded, - replacement: replacement, - child: child!, - ); - }, - child: child, - ), - ); - }, + final mapHeight = context.select((v) => v.mapHeight); + child = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + mapHeight != null + ? SizedBox( + height: mapHeight, + child: child, + ) + : Expanded(child: child), + SafeArea( + top: false, + bottom: false, + child: Attribution(style: mapStyle), + ), + ], + ); + + return AnimatedSize( + alignment: Alignment.topCenter, + curve: Curves.easeInOutCubic, + duration: Durations.mapStyleSwitchAnimation, + child: ValueListenableBuilder( + valueListenable: widget.isAnimatingNotifier, + builder: (context, animating, child) { + if (!animating && isGoogleMaps) { + _googleMapsLoaded = true; + } + Widget replacement = Stack( + children: [ + const MapDecorator(), + MapButtonPanel( + boundsNotifier: _boundsNotifier, + openMapPage: widget.openMapPage, + ), + ], + ); + if (mapHeight != null) { + replacement = SizedBox( + height: mapHeight, + child: replacement, + ); + } + return Visibility( + visible: !isGoogleMaps || _googleMapsLoaded, + replacement: replacement, + child: child!, + ); + }, + child: child, + ), ); }, ); diff --git a/lib/widgets/common/map/google/map.dart b/lib/widgets/common/map/google/map.dart index c00462773..1cf44550d 100644 --- a/lib/widgets/common/map/google/map.dart +++ b/lib/widgets/common/map/google/map.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/common/map/buttons.dart'; import 'package:aves/widgets/common/map/controller.dart'; diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index f04a5d464..600a10f1b 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; diff --git a/lib/widgets/common/map/leaflet/scale_layer.dart b/lib/widgets/common/map/leaflet/scale_layer.dart index 32b0dd69b..37e76093c 100644 --- a/lib/widgets/common/map/leaflet/scale_layer.dart +++ b/lib/widgets/common/map/leaflet/scale_layer.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/common/basic/outlined_text.dart'; import 'package:aves/widgets/common/map/leaflet/scalebar_utils.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/map/leaflet/tile_layers.dart b/lib/widgets/common/map/leaflet/tile_layers.dart index 759502bcb..069a0967c 100644 --- a/lib/widgets/common/map/leaflet/tile_layers.dart +++ b/lib/widgets/common/map/leaflet/tile_layers.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:provider/provider.dart'; +const _tileLayerBackgroundColor = Colors.transparent; + class OSMHotLayer extends StatelessWidget { const OSMHotLayer({Key? key}) : super(key: key); @@ -12,6 +14,7 @@ class OSMHotLayer extends StatelessWidget { options: TileLayerOptions( urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], + backgroundColor: _tileLayerBackgroundColor, tileProvider: _NetworkTileProvider(), retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, ), @@ -28,6 +31,7 @@ class StamenTonerLayer extends StatelessWidget { options: TileLayerOptions( urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png', subdomains: ['a', 'b', 'c', 'd'], + backgroundColor: _tileLayerBackgroundColor, tileProvider: _NetworkTileProvider(), retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, ), @@ -44,6 +48,7 @@ class StamenWatercolorLayer extends StatelessWidget { options: TileLayerOptions( urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg', subdomains: ['a', 'b', 'c', 'd'], + backgroundColor: _tileLayerBackgroundColor, tileProvider: _NetworkTileProvider(), retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, ), diff --git a/lib/widgets/common/thumbnail/decorated.dart b/lib/widgets/common/thumbnail/decorated.dart index bcf87ee42..5c0b65f39 100644 --- a/lib/widgets/common/thumbnail/decorated.dart +++ b/lib/widgets/common/thumbnail/decorated.dart @@ -27,7 +27,6 @@ class DecoratedThumbnail extends StatelessWidget { @override Widget build(BuildContext context) { - final isSvg = entry.isSvg; Widget child = ThumbnailImage( entry: entry, extent: tileExtent, @@ -36,10 +35,10 @@ class DecoratedThumbnail extends StatelessWidget { ); child = Stack( - alignment: isSvg ? Alignment.center : AlignmentDirectional.bottomStart, + fit: StackFit.passthrough, children: [ child, - if (!isSvg) ThumbnailEntryOverlay(entry: entry), + ThumbnailEntryOverlay(entry: entry), if (selectable) GridItemSelectionOverlay( item: entry, diff --git a/lib/widgets/common/thumbnail/error.dart b/lib/widgets/common/thumbnail/error.dart index b1015de3e..6ca741e0d 100644 --- a/lib/widgets/common/thumbnail/error.dart +++ b/lib/widgets/common/thumbnail/error.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/mime_utils.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -58,14 +57,10 @@ class _ErrorThumbnailState extends State { textAlign: TextAlign.center, ); }) - : Tooltip( - message: context.l10n.viewerErrorDoesNotExist, - preferBelow: false, - child: Icon( - AIcons.broken, - size: extent / 2, - color: color, - ), + : Icon( + AIcons.broken, + size: extent / 2, + color: color, ); } return Container( diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index 3edba69d8..0a6961070 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -4,9 +4,9 @@ import 'dart:ui'; import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/settings/accessibility_animations.dart'; -import 'package:aves/model/settings/entry_background.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/entry_background.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; diff --git a/lib/widgets/common/thumbnail/overlay.dart b/lib/widgets/common/thumbnail/overlay.dart index 28b8251b5..39a4f60b9 100644 --- a/lib/widgets/common/thumbnail/overlay.dart +++ b/lib/widgets/common/thumbnail/overlay.dart @@ -35,13 +35,16 @@ class ThumbnailEntryOverlay extends StatelessWidget { if (entry.isMotionPhoto && context.select((t) => t.showMotionPhoto)) const MotionPhotoIcon(), if (!entry.isMotionPhoto) MultiPageIcon(entry: entry), ], + if (entry.trashed && context.select((t) => t.showTrash)) TrashIcon(trashDaysLeft: entry.trashDaysLeft), ]; if (children.isEmpty) return const SizedBox(); - if (children.length == 1) return children.first; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: children, + return Align( + alignment: AlignmentDirectional.bottomStart, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), ); } } diff --git a/lib/widgets/common/thumbnail/scroller.dart b/lib/widgets/common/thumbnail/scroller.dart index b04af682d..6b8e14a74 100644 --- a/lib/widgets/common/thumbnail/scroller.dart +++ b/lib/widgets/common/thumbnail/scroller.dart @@ -86,6 +86,7 @@ class _ThumbnailScrollerState extends State { return GridTheme( extent: extent, showLocation: false, + showTrash: false, child: SizedBox( height: extent, child: ListView.separated( diff --git a/lib/widgets/common/tile_extent_controller.dart b/lib/widgets/common/tile_extent_controller.dart index 1e39af61e..5a055a2ed 100644 --- a/lib/widgets/common/tile_extent_controller.dart +++ b/lib/widgets/common/tile_extent_controller.dart @@ -20,7 +20,7 @@ class TileExtentController { this.columnCountMin = 2, required this.columnCountDefault, required this.extentMin, - this.extentMax = 300, + required this.extentMax, required this.spacing, }) { userPreferredExtent = settings.getTileExtent(settingsRouteKey); diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 3ad653c3f..7223094ec 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -8,6 +8,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/analysis_service.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -131,12 +132,20 @@ class _AppDebugPageState extends State { title: const Text('Show tasks overlay'), ), ElevatedButton( - onPressed: () async { - final source = context.read(); - await source.init(); - await source.refresh(); - }, - child: const Text('Source full refresh'), + onPressed: () => source.init(loadTopEntriesFirst: false), + child: const Text('Source refresh (top off)'), + ), + ElevatedButton( + onPressed: () => source.init(loadTopEntriesFirst: true), + child: const Text('Source refresh (top on)'), + ), + ElevatedButton( + onPressed: () => source.init(directory: '${androidFileUtils.dcimPath}/Camera'), + child: const Text('Source refresh (camera)'), + ), + ElevatedButton( + onPressed: () => source.init(directory: androidFileUtils.picturesPath), + child: const Text('Source refresh (pictures)'), ), ElevatedButton( onPressed: () => AnalysisService.startService(force: false), @@ -163,18 +172,16 @@ class _AppDebugPageState extends State { Future _onActionSelected(AppDebugAction action) async { switch (action) { case AppDebugAction.prepScreenshotThumbnails: - final source = context.read(); - source.changeFilterVisibility(settings.hiddenFilters, true); - source.changeFilterVisibility({ + settings.changeFilterVisibility(settings.hiddenFilters, true); + settings.changeFilterVisibility({ TagFilter('aves-thumbnail', not: true), }, false); await favourites.clear(); await favourites.add(source.visibleEntries); break; case AppDebugAction.prepScreenshotStats: - final source = context.read(); - source.changeFilterVisibility(settings.hiddenFilters, true); - source.changeFilterVisibility({ + settings.changeFilterVisibility(settings.hiddenFilters, true); + settings.changeFilterVisibility({ PathFilter('/storage/emulated/0/Pictures/Dev'), }, false); break; diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index d734ac5d5..f2bf3386b 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -3,6 +3,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; +import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/video_playback.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/file_utils.dart'; @@ -20,8 +21,9 @@ class _DebugAppDatabaseSectionState extends State with late Future _dbFileSizeLoader; late Future> _dbEntryLoader; late Future> _dbDateLoader; - late Future> _dbMetadataLoader; - late Future> _dbAddressLoader; + late Future> _dbMetadataLoader; + late Future> _dbAddressLoader; + late Future> _dbTrashLoader; late Future> _dbFavouritesLoader; late Future> _dbCoversLoader; late Future> _dbVideoPlaybackLoader; @@ -106,7 +108,7 @@ class _DebugAppDatabaseSectionState extends State with ); }, ), - FutureBuilder( + FutureBuilder( future: _dbMetadataLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); @@ -120,14 +122,14 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearMetadataEntries().then((_) => _startDbReport()), + onPressed: () => metadataDb.clearCatalogMetadata().then((_) => _startDbReport()), child: const Text('Clear'), ), ], ); }, ), - FutureBuilder( + FutureBuilder( future: _dbAddressLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); @@ -148,6 +150,27 @@ class _DebugAppDatabaseSectionState extends State with ); }, ), + FutureBuilder( + future: _dbTrashLoader, + builder: (context, snapshot) { + if (snapshot.hasError) return Text(snapshot.error.toString()); + + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + + return Row( + children: [ + Expanded( + child: Text('trash rows: ${snapshot.data!.length}'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () => metadataDb.clearTrashDetails().then((_) => _startDbReport()), + child: const Text('Clear'), + ), + ], + ); + }, + ), FutureBuilder( future: _dbFavouritesLoader, builder: (context, snapshot) { @@ -220,10 +243,11 @@ class _DebugAppDatabaseSectionState extends State with void _startDbReport() { _dbFileSizeLoader = metadataDb.dbFileSize(); - _dbEntryLoader = metadataDb.loadAllEntries(); + _dbEntryLoader = metadataDb.loadEntries(); _dbDateLoader = metadataDb.loadDates(); - _dbMetadataLoader = metadataDb.loadAllMetadataEntries(); - _dbAddressLoader = metadataDb.loadAllAddresses(); + _dbMetadataLoader = metadataDb.loadCatalogMetadata(); + _dbAddressLoader = metadataDb.loadAddresses(); + _dbTrashLoader = metadataDb.loadAllTrashDetails(); _dbFavouritesLoader = metadataDb.loadAllFavourites(); _dbCoversLoader = metadataDb.loadAllCovers(); _dbVideoPlaybackLoader = metadataDb.loadAllVideoPlayback(); diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index de9cf5806..81bded031 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -68,6 +68,7 @@ class DebugSettingsSection extends StatelessWidget { 'searchHistory': toMultiline(settings.searchHistory), 'locale': '${settings.locale}', 'systemLocales': '${WidgetsBinding.instance!.window.locales}', + 'topEntryIds': '${settings.topEntryIds}', }, ), ), diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index 94cbbbbd3..42ce29bb7 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -38,7 +38,7 @@ class _AddShortcutDialogState extends State { if (_collection != null) { final entries = _collection.sortedEntries; if (entries.isNotEmpty) { - final coverEntries = _collection.filters.map(covers.coverContentId).whereNotNull().map((id) => entries.firstWhereOrNull((entry) => entry.contentId == id)).whereNotNull(); + final coverEntries = _collection.filters.map(covers.coverEntryId).whereNotNull().map((id) => entries.firstWhereOrNull((entry) => entry.id == id)).whereNotNull(); _coverEntry = coverEntries.firstOrNull ?? entries.first; } } diff --git a/lib/widgets/dialogs/aves_confirmation_dialog.dart b/lib/widgets/dialogs/aves_confirmation_dialog.dart new file mode 100644 index 000000000..6d51c812e --- /dev/null +++ b/lib/widgets/dialogs/aves_confirmation_dialog.dart @@ -0,0 +1,79 @@ +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; + +import 'aves_dialog.dart'; + +Future showConfirmationDialog({ + required BuildContext context, + required ConfirmationDialog type, + required String message, + required String confirmationButtonLabel, +}) async { + if (!settings.confirmationDialogs.contains(type)) return true; + + final confirmed = await showDialog( + context: context, + builder: (context) => AvesConfirmationDialog( + type: type, + message: message, + confirmationButtonLabel: confirmationButtonLabel, + ), + ); + return confirmed == true; +} + +class AvesConfirmationDialog extends StatefulWidget { + final ConfirmationDialog type; + final String message, confirmationButtonLabel; + + const AvesConfirmationDialog({ + Key? key, + required this.type, + required this.message, + required this.confirmationButtonLabel, + }) : super(key: key); + + @override + _AvesConfirmationDialogState createState() => _AvesConfirmationDialogState(); +} + +class _AvesConfirmationDialogState extends State { + final ValueNotifier _skipConfirmation = ValueNotifier(false); + + @override + Widget build(BuildContext context) { + return AvesDialog( + scrollableContent: [ + Padding( + padding: const EdgeInsets.all(16) + const EdgeInsets.only(top: 8), + child: Text(widget.message), + ), + ValueListenableBuilder( + valueListenable: _skipConfirmation, + builder: (context, ask, child) => SwitchListTile( + value: ask, + onChanged: (v) => _skipConfirmation.value = v, + title: Text(context.l10n.doNotAskAgain), + ), + ), + ], + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + TextButton( + onPressed: () { + if (_skipConfirmation.value) { + settings.confirmationDialogs = settings.confirmationDialogs.toList()..remove(widget.type); + } + Navigator.pop(context, true); + }, + child: Text(widget.confirmationButtonLabel), + ), + ], + ); + } +} diff --git a/lib/widgets/dialogs/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart index 020a70956..cdc9d6d2a 100644 --- a/lib/widgets/dialogs/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -44,7 +44,7 @@ class AvesDialog extends StatelessWidget { // and overflow feedback ignores the dialog shape, // so we restrict scrolling to the content instead content: _buildContent(context), - contentPadding: scrollableContent != null ? EdgeInsets.zero : EdgeInsets.fromLTRB(horizontalContentPadding, 20, horizontalContentPadding, 0), + contentPadding: scrollableContent != null ? EdgeInsets.zero : EdgeInsets.only(left: horizontalContentPadding, top: 20, right: horizontalContentPadding), actions: actions, actionsPadding: const EdgeInsets.symmetric(horizontal: 8), shape: shape(context), diff --git a/lib/widgets/dialogs/export_entry_dialog.dart b/lib/widgets/dialogs/export_entry_dialog.dart index 1fc67ee6e..d84b6c10d 100644 --- a/lib/widgets/dialogs/export_entry_dialog.dart +++ b/lib/widgets/dialogs/export_entry_dialog.dart @@ -51,12 +51,14 @@ class _ExportEntryDialogState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: AvesDialog.defaultHorizontalContentPadding); + return AvesDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + scrollableContent: [ + const SizedBox(height: 16), + Padding( + padding: contentHorizontalPadding, + child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(l10n.exportEntryDialogFormat), @@ -77,7 +79,10 @@ class _ExportEntryDialogState extends State { ), ], ), - Row( + ), + Padding( + padding: contentHorizontalPadding, + child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ @@ -108,8 +113,9 @@ class _ExportEntryDialogState extends State { ), ], ), - ], - ), + ), + const SizedBox(height: 16), + ], actions: [ TextButton( onPressed: () => Navigator.pop(context), diff --git a/lib/widgets/dialogs/item_pick_dialog.dart b/lib/widgets/dialogs/item_pick_dialog.dart index c72252e22..7623c05e1 100644 --- a/lib/widgets/dialogs/item_pick_dialog.dart +++ b/lib/widgets/dialogs/item_pick_dialog.dart @@ -39,7 +39,7 @@ class _ItemPickDialogState extends State { Widget build(BuildContext context) { final liveFilter = collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; return ListenableProvider>.value( - value: ValueNotifier(AppMode.pickInternal), + value: ValueNotifier(AppMode.pickMediaInternal), child: MediaQueryDataProvider( child: Scaffold( body: SelectionProvider( diff --git a/lib/widgets/dialogs/location_pick_dialog.dart b/lib/widgets/dialogs/location_pick_dialog.dart index ecdd98f5d..ce0cf1e65 100644 --- a/lib/widgets/dialogs/location_pick_dialog.dart +++ b/lib/widgets/dialogs/location_pick_dialog.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:aves/model/settings/coordinate_format.dart'; -import 'package:aves/model/settings/map_style.dart'; +import 'package:aves/model/settings/enums/coordinate_format.dart'; +import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/common/services.dart'; diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index 3604fa3e0..2d08bb60a 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -1,13 +1,17 @@ import 'dart:ui'; +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; @@ -20,12 +24,19 @@ import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/settings/settings_page.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class AppDrawer extends StatelessWidget { - const AppDrawer({Key? key}) : super(key: key); + // collection loaded in the `CollectionPage`, if any + final CollectionLens? currentCollection; + + const AppDrawer({ + Key? key, + this.currentCollection, + }) : super(key: key); static List getDefaultAlbums(BuildContext context) { final source = context.read(); @@ -44,6 +55,10 @@ class AppDrawer extends StatelessWidget { ..._buildTypeLinks(), _buildAlbumLinks(context), ..._buildPageLinks(context), + if (settings.enableBin) ...[ + const Divider(), + binTile(context), + ], if (!kReleaseMode) ...[ const Divider(), debugTile, @@ -153,6 +168,7 @@ class AppDrawer extends StatelessWidget { List _buildTypeLinks() { final hiddenFilters = settings.hiddenFilters; final typeBookmarks = settings.drawerTypeBookmarks; + final currentFilters = currentCollection?.filters; return typeBookmarks .where((filter) => !hiddenFilters.contains(filter)) .map((filter) => CollectionNavTile( @@ -161,12 +177,17 @@ class AppDrawer extends StatelessWidget { leading: DrawerFilterIcon(filter: filter), title: DrawerFilterTitle(filter: filter), filter: filter, + isSelected: () { + if (currentFilters == null || currentFilters.length > 1) return false; + return currentFilters.firstOrNull == filter; + }, )) .toList(); } Widget _buildAlbumLinks(BuildContext context) { final source = context.read(); + final currentFilters = currentCollection?.filters; return StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) { @@ -175,7 +196,14 @@ class AppDrawer extends StatelessWidget { return Column( children: [ const Divider(), - ...albums.map((album) => AlbumNavTile(album: album)), + ...albums.map((album) => AlbumNavTile( + album: album, + isSelected: () { + if (currentFilters == null || currentFilters.length > 1) return false; + final currentFilter = currentFilters.firstOrNull; + return currentFilter is AlbumFilter && currentFilter.album == album; + }, + )), ], ); }); @@ -226,6 +254,20 @@ class AppDrawer extends StatelessWidget { ]; } + Widget binTile(BuildContext context) { + final source = context.read(); + final trashSize = source.trashedEntries.fold(0, (sum, entry) => sum + (entry.sizeBytes ?? 0)); + + const filter = TrashFilter.instance; + return CollectionNavTile( + leading: const DrawerFilterIcon(filter: filter), + title: const DrawerFilterTitle(filter: filter), + trailing: Text(formatFileSize(context.l10n.localeName, trashSize, round: 0)), + filter: filter, + isSelected: () => currentCollection?.filters.contains(filter) ?? false, + ); + } + Widget get debugTile => PageNavTile( // key is expected by test driver key: const Key('drawer-debug'), diff --git a/lib/widgets/drawer/collection_nav_tile.dart b/lib/widgets/drawer/collection_nav_tile.dart index f8fa85239..8d264f276 100644 --- a/lib/widgets/drawer/collection_nav_tile.dart +++ b/lib/widgets/drawer/collection_nav_tile.dart @@ -5,6 +5,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/drawer/tile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -15,6 +16,7 @@ class CollectionNavTile extends StatelessWidget { final Widget? trailing; final bool dense; final CollectionFilter? filter; + final bool Function() isSelected; const CollectionNavTile({ Key? key, @@ -23,6 +25,7 @@ class CollectionNavTile extends StatelessWidget { this.trailing, bool? dense, required this.filter, + required this.isSelected, }) : dense = dense ?? false, super(key: key); @@ -34,9 +37,23 @@ class CollectionNavTile extends StatelessWidget { child: ListTile( leading: leading, title: title, - trailing: trailing, + trailing: trailing != null + ? Builder( + builder: (context) { + final trailingColor = IconTheme.of(context).color!.withOpacity(.6); + return IconTheme.merge( + data: IconThemeData(color: trailingColor), + child: DefaultTextStyle.merge( + style: TextStyle(color: trailingColor), + child: trailing!, + ), + ); + }, + ) + : null, dense: dense, onTap: () => _goToCollection(context), + selected: context.currentRouteName == CollectionPage.routeName && isSelected(), ), ); } @@ -61,10 +78,12 @@ class CollectionNavTile extends StatelessWidget { class AlbumNavTile extends StatelessWidget { final String album; + final bool Function() isSelected; const AlbumNavTile({ Key? key, required this.album, + required this.isSelected, }) : super(key: key); @override @@ -78,10 +97,10 @@ class AlbumNavTile extends StatelessWidget { ? const Icon( AIcons.removableStorage, size: 16, - color: Colors.grey, ) : null, filter: filter, + isSelected: isSelected, ); } } diff --git a/lib/widgets/drawer/page_nav_tile.dart b/lib/widgets/drawer/page_nav_tile.dart index 465d1ac33..938b43367 100644 --- a/lib/widgets/drawer/page_nav_tile.dart +++ b/lib/widgets/drawer/page_nav_tile.dart @@ -40,20 +40,18 @@ class PageNavTile extends StatelessWidget { onTap: _pageBuilder != null ? () { Navigator.pop(context); - if (routeName != context.currentRouteName) { - final route = MaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: _pageBuilder, + final route = MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: _pageBuilder, + ); + if (topLevel) { + Navigator.pushAndRemoveUntil( + context, + route, + (route) => false, ); - if (topLevel) { - Navigator.pushAndRemoveUntil( - context, - route, - (route) => false, - ); - } else { - Navigator.push(context, route); - } + } else { + Navigator.push(context, route); } } : null, diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index 715ddd76b..fd4c97d8d 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -2,8 +2,8 @@ import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/type.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -77,7 +77,7 @@ class DrawerPageIcon extends StatelessWidget { return const Icon(AIcons.tag); case AppDebugPage.routeName: return ShaderMask( - shaderCallback: Themes.debugGradient.createShader, + shaderCallback: AColors.debugGradient.createShader, child: const Icon(AIcons.debug), ); default: diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 63755740b..00c404f5c 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -65,7 +65,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { @override Widget build(BuildContext context) { return ListenableProvider>.value( - value: ValueNotifier(AppMode.pickInternal), + value: ValueNotifier(AppMode.pickFilterInternal), child: Selector>( selector: (context, s) => Tuple2(s.albumGroupFactor, s.albumSortFactor), builder: (context, s, child) { @@ -131,11 +131,13 @@ class _AlbumPickAppBar extends StatelessWidget { switch (moveType) { case MoveType.copy: return context.l10n.albumPickPageTitleCopy; - case MoveType.export: - return context.l10n.albumPickPageTitleExport; case MoveType.move: return context.l10n.albumPickPageTitleMove; - default: + case MoveType.export: + return context.l10n.albumPickPageTitleExport; + case MoveType.toBin: + case MoveType.fromBin: + case null: return context.l10n.albumPickPageTitlePick; } } diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index 786d82e0c..bc49eebac 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -15,6 +15,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; @@ -28,7 +29,7 @@ import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class AlbumChipSetActionDelegate extends ChipSetActionDelegate { +class AlbumChipSetActionDelegate extends ChipSetActionDelegate with EntryStorageMixin { final Iterable> _items; AlbumChipSetActionDelegate(Iterable> items) : _items = items; @@ -181,15 +182,30 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate { } Future _delete(BuildContext context, Set filters) async { - final l10n = context.l10n; - final messenger = ScaffoldMessenger.of(context); final source = context.read(); final todoEntries = source.visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet(); - final todoCount = todoEntries.length; final todoAlbums = filters.map((v) => v.album).toSet(); final filledAlbums = todoEntries.map((e) => e.directory).whereNotNull().toSet(); final emptyAlbums = todoAlbums.whereNot(filledAlbums.contains).toSet(); + if (settings.enableBin && filledAlbums.isNotEmpty) { + await move( + context, + moveType: MoveType.toBin, + entries: todoEntries, + onSuccess: () { + source.forgetNewAlbums(todoAlbums); + source.cleanEmptyAlbums(emptyAlbums); + _browse(context); + }, + ); + return; + } + + final l10n = context.l10n; + final messenger = ScaffoldMessenger.of(context); + final todoCount = todoEntries.length; + final confirmed = await showDialog( context: context, builder: (context) { @@ -217,7 +233,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate { source.pauseMonitoring(); final opId = mediaFileService.newOpId; - showOpReport( + await showOpReport( context: context, opStream: mediaFileService.delete(opId: opId, entries: todoEntries), itemCount: todoCount, @@ -226,7 +242,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate { final successOps = processed.where((event) => event.success); final deletedOps = successOps.where((e) => !e.skipped).toSet(); final deletedUris = deletedOps.map((event) => event.uri).toSet(); - await source.removeEntries(deletedUris); + await source.removeEntries(deletedUris, includeTrash: true); _browse(context); source.resumeMonitoring(); @@ -281,13 +297,12 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate { source.pauseMonitoring(); final opId = mediaFileService.newOpId; - showOpReport( + await showOpReport( context: context, opStream: mediaFileService.move( opId: opId, - entries: todoEntries, + entriesByDestination: {destinationAlbum: todoEntries}, copy: false, - destinationAlbum: destinationAlbum, // there should be no file conflict, as the target directory itself does not exist nameConflictStrategy: NameConflictStrategy.rename, ), diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart index bc5c1de97..3e86c12db 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart @@ -1,7 +1,7 @@ import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -51,8 +51,7 @@ class ChipActionDelegate { ); if (confirmed == null || !confirmed) return; - final source = context.read(); - source.changeFilterVisibility({filter}, false); + settings.changeFilterVisibility({filter}, false); } void _goTo( diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index c55aa8d8d..f20f02796 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -271,15 +271,14 @@ abstract class ChipSetActionDelegate with FeedbackMi ); if (confirmed == null || !confirmed) return; - final source = context.read(); - source.changeFilterVisibility(filters, false); + settings.changeFilterVisibility(filters, false); _browse(context); } void _setCover(BuildContext context, T filter) async { - final contentId = covers.coverContentId(filter); - final customEntry = context.read().visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId); + final entryId = covers.coverEntryId(filter); + final customEntry = context.read().visibleEntries.firstWhereOrNull((entry) => entry.id == entryId); final coverSelection = await showDialog>( context: context, builder: (context) => CoverSelectionDialog( @@ -290,7 +289,7 @@ abstract class ChipSetActionDelegate with FeedbackMi if (coverSelection == null) return; final isCustom = coverSelection.item1; - await covers.set(filter, isCustom ? coverSelection.item2?.contentId : null); + await covers.set(filter, isCustom ? coverSelection.item2?.id : null); _browse(context); } diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index 463fb19e5..5023a70ac 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -110,9 +110,10 @@ class _FilterGridAppBarState extends State>, int>( selector: (context, selection) => selection.selectedItems.length, - builder: (context, count, child) => Text(context.l10n.collectionSelectionPageTitle(count)), + builder: (context, count, child) => Text(count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count)), ); } else { final appMode = context.watch>().value; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index e9e098ddc..387513474 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -164,6 +164,7 @@ class _FilterGridState extends State> settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName!, columnCountDefault: 3, extentMin: 60, + extentMax: 300, spacing: 8, ); return TileExtentControllerProvider( diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index a3e01625e..f175a3101 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -51,6 +51,7 @@ class _InteractiveFilterTileState extends State>().value; switch (appMode) { case AppMode.main: + case AppMode.pickMediaExternal: final selection = context.read>>(); if (selection.isSelecting) { selection.toggleSelection(gridItem); @@ -58,10 +59,10 @@ class _InteractiveFilterTileState extends State(context, filter); break; - case AppMode.pickExternal: + case AppMode.pickMediaInternal: case AppMode.view: break; } diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index aeb9d078f..a94900d38 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -4,7 +4,8 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/settings/home_page.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/home_page.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -94,7 +95,7 @@ class _HomePageState extends State { } break; case 'pick': - appMode = AppMode.pickExternal; + appMode = AppMode.pickMediaExternal; // TODO TLAD apply pick mimetype(s) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) String? pickMimeTypes = intentData['mimeType']; @@ -116,14 +117,33 @@ class _HomePageState extends State { } context.read>().value = appMode; unawaited(reportService.setCustomKey('app_mode', appMode.toString())); + debugPrint('Storage check complete in ${stopwatch.elapsed.inMilliseconds}ms'); - if (appMode != AppMode.view || _isViewerSourceable(_viewerEntry!)) { - debugPrint('Storage check complete in ${stopwatch.elapsed.inMilliseconds}ms'); - unawaited(GlobalSearch.registerCallback()); - unawaited(AnalysisService.registerCallback()); - final source = context.read(); - await source.init(); - unawaited(source.refresh()); + switch (appMode) { + case AppMode.main: + case AppMode.pickMediaExternal: + unawaited(GlobalSearch.registerCallback()); + unawaited(AnalysisService.registerCallback()); + final source = context.read(); + await source.init( + loadTopEntriesFirst: settings.homePage == HomePageSetting.collection, + ); + break; + case AppMode.view: + if (_isViewerSourceable(_viewerEntry)) { + final directory = _viewerEntry?.directory; + if (directory != null) { + unawaited(AnalysisService.registerCallback()); + final source = context.read(); + await source.init( + directory: directory, + ); + } + } + break; + case AppMode.pickMediaInternal: + case AppMode.pickFilterInternal: + break; } // `pushReplacement` is not enough in some edge cases @@ -135,7 +155,9 @@ class _HomePageState extends State { )); } - bool _isViewerSourceable(AvesEntry viewerEntry) => viewerEntry.directory != null && !settings.hiddenFilters.any((filter) => filter.test(viewerEntry)); + bool _isViewerSourceable(AvesEntry? viewerEntry) { + return viewerEntry != null && viewerEntry.directory != null && !settings.hiddenFilters.any((filter) => filter.test(viewerEntry)); + } Future _initViewerEntry({required String uri, required String? mimeType}) async { if (uri.startsWith('/')) { @@ -156,7 +178,7 @@ class _HomePageState extends State { CollectionLens? collection; final source = context.read(); - if (source.initialized) { + if (source.initState != SourceInitializationState.none) { final album = viewerEntry.directory; if (album != null) { // wait for collection to pass the `loading` state @@ -174,6 +196,11 @@ class _HomePageState extends State { collection = CollectionLens( source: source, filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))}, + listenToSource: false, + // if we group bursts, opening a burst sub-entry should: + // - identify and select the containing main entry, + // - select the sub-entry in the Viewer page. + groupBursts: false, ); final viewerEntryPath = viewerEntry.path; final collectionEntry = collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath); @@ -199,7 +226,7 @@ class _HomePageState extends State { String routeName; Set? filters; - if (appMode == AppMode.pickExternal) { + if (appMode == AppMode.pickMediaExternal) { routeName = CollectionPage.routeName; } else { routeName = _shortcutRouteName ?? settings.homePage.routeName; diff --git a/lib/widgets/map/map_info_row.dart b/lib/widgets/map/map_info_row.dart index 53ad3c54f..093acf11a 100644 --- a/lib/widgets/map/map_info_row.dart +++ b/lib/widgets/map/map_info_row.dart @@ -1,5 +1,5 @@ import 'package:aves/model/entry.dart'; -import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/geocoding_service.dart'; diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 171eafe28..29a2db70d 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -1,11 +1,12 @@ import 'dart:async'; +import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/coordinate.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/settings/enums.dart'; -import 'package:aves/model/settings/map_style.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; @@ -83,7 +84,7 @@ class _Content extends StatefulWidget { class _ContentState extends State<_Content> with SingleTickerProviderStateMixin { final List _subscriptions = []; final AvesMapController _mapController = AvesMapController(); - late final ValueNotifier _isPageAnimatingNotifier; + final ValueNotifier _isPageAnimatingNotifier = ValueNotifier(false); final ValueNotifier _selectedIndexNotifier = ValueNotifier(0); final ValueNotifier _regionCollectionNotifier = ValueNotifier(null); final ValueNotifier _dotLocationNotifier = ValueNotifier(null); @@ -102,13 +103,11 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin super.initState(); if (settings.infoMapStyle.isGoogleMaps) { - _isPageAnimatingNotifier = ValueNotifier(true); + _isPageAnimatingNotifier.value = true; Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { if (!mounted) return; _isPageAnimatingNotifier.value = false; }); - } else { - _isPageAnimatingNotifier = ValueNotifier(false); } _dotEntryNotifier.addListener(_updateInfoEntry); @@ -269,7 +268,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin entryBuilder: (index) => index < regionEntries.length ? regionEntries[index] : null, indexNotifier: _selectedIndexNotifier, onTap: _onThumbnailTap, - heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.uri]), + heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.id]), highlightable: true, ); }, @@ -326,7 +325,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin } AvesEntry? _getRegionEntry(int? index) { - if (index != null && regionCollection != null) { + if (index != null && index >= 0 && regionCollection != null) { final regionEntries = regionCollection!.sortedEntries; if (index < regionEntries.length) { return regionEntries[index]; @@ -373,6 +372,9 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin } void _goToCollection(CollectionFilter filter) { + final isMainMode = context.read>().value == AppMode.main; + if (!isMainMode) return; + Navigator.pushAndRemoveUntil( context, MaterialPageRoute( diff --git a/lib/widgets/settings/accessibility/accessibility.dart b/lib/widgets/settings/accessibility/accessibility.dart index 3d65b0417..e4fd0aadb 100644 --- a/lib/widgets/settings/accessibility/accessibility.dart +++ b/lib/widgets/settings/accessibility/accessibility.dart @@ -1,5 +1,5 @@ +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/accessibility/remove_animations.dart'; @@ -20,7 +20,7 @@ class AccessibilitySection extends StatelessWidget { return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.accessibility, - color: stringToColor('Accessibility'), + color: AColors.accessibility, ), title: context.l10n.settingsSectionAccessibility, expandedNotifier: expandedNotifier, diff --git a/lib/widgets/settings/accessibility/remove_animations.dart b/lib/widgets/settings/accessibility/remove_animations.dart index bf257867f..bf3095cef 100644 --- a/lib/widgets/settings/accessibility/remove_animations.dart +++ b/lib/widgets/settings/accessibility/remove_animations.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/settings/accessibility_animations.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/widgets/settings/accessibility/time_to_take_action.dart b/lib/widgets/settings/accessibility/time_to_take_action.dart index 75ceb0cb9..ced0e082d 100644 --- a/lib/widgets/settings/accessibility/time_to_take_action.dart +++ b/lib/widgets/settings/accessibility/time_to_take_action.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/settings/accessibility_timeout.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/accessibility_timeout.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index e3d87797a..874441321 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -97,7 +97,7 @@ class _QuickActionEditorBodyState extends State const ConfirmationDialogPage(), + ), + ); + }, + ); + } +} + +class ConfirmationDialogPage extends StatelessWidget { + static const routeName = '/settings/navigation_confirmation'; + + const ConfirmationDialogPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.settingsConfirmationDialogTitle), + ), + body: SafeArea( + child: Selector>( + selector: (context, s) => s.confirmationDialogs, + builder: (context, current, child) => ListView( + children: [ + ConfirmationDialog.moveToBin, + ConfirmationDialog.delete, + ] + .map((dialog) => SwitchListTile( + value: current.contains(dialog), + onChanged: (v) { + final dialogs = current.toList(); + if (v) { + dialogs.add(dialog); + } else { + dialogs.remove(dialog); + } + settings.confirmationDialogs = dialogs; + }, + title: Text(dialog.getName(context)), + )) + .toList(), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index 50f8252fa..a7466c80c 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -1,13 +1,14 @@ -import 'package:aves/model/settings/enums.dart'; -import 'package:aves/model/settings/home_page.dart'; -import 'package:aves/model/settings/screen_on.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/home_page.dart'; +import 'package:aves/model/settings/enums/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/navigation/confirmation_dialogs.dart'; import 'package:aves/widgets/settings/navigation/drawer.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -29,7 +30,7 @@ class NavigationSection extends StatelessWidget { return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.home, - color: stringToColor('Navigation'), + color: AColors.navigation, ), title: context.l10n.settingsSectionNavigation, expandedNotifier: expandedNotifier, @@ -53,6 +54,7 @@ class NavigationSection extends StatelessWidget { }, ), const NavigationDrawerTile(), + const ConfirmationDialogTile(), ListTile( title: Text(context.l10n.settingsKeepScreenOnTile), subtitle: Text(currentKeepScreenOn.getName(context)), diff --git a/lib/widgets/settings/privacy/hidden_items.dart b/lib/widgets/settings/privacy/hidden_items.dart index 0c1b0f17b..7073702d1 100644 --- a/lib/widgets/settings/privacy/hidden_items.dart +++ b/lib/widgets/settings/privacy/hidden_items.dart @@ -1,7 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/path.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -116,7 +115,7 @@ class _HiddenFilters extends StatelessWidget { .map((filter) => AvesFilterChip( filter: filter, removable: true, - onTap: (filter) => context.read().changeFilterVisibility({filter}, true), + onTap: (filter) => settings.changeFilterVisibility({filter}, true), onLongPress: null, )) .toList(), @@ -152,7 +151,7 @@ class _HiddenPaths extends StatelessWidget { trailing: IconButton( icon: const Icon(AIcons.clear), onPressed: () { - context.read().changeFilterVisibility({pathFilter}, true); + settings.changeFilterVisibility({pathFilter}, true); }, tooltip: context.l10n.actionRemove, ), @@ -176,7 +175,7 @@ class _HiddenPaths extends StatelessWidget { // wait for the dialog to hide as applying the change may block the UI await Future.delayed(Durations.pageTransitionAnimation * timeDilation); if (path != null && path.isNotEmpty) { - context.read().changeFilterVisibility({PathFilter(path)}, false); + settings.changeFilterVisibility({PathFilter(path)}, false); } }, ), diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart index d00919fc2..327326d67 100644 --- a/lib/widgets/settings/privacy/privacy.dart +++ b/lib/widgets/settings/privacy/privacy.dart @@ -1,8 +1,8 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; @@ -26,7 +26,7 @@ class PrivacySection extends StatelessWidget { return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.privacy, - color: stringToColor('Privacy'), + color: AColors.privacy, ), title: context.l10n.settingsSectionPrivacy, expandedNotifier: expandedNotifier, @@ -63,6 +63,20 @@ class PrivacySection extends StatelessWidget { title: Text(context.l10n.settingsSaveSearchHistory), ), ), + Selector( + selector: (context, s) => s.enableBin, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) { + settings.enableBin = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: Text(context.l10n.settingsEnableBin), + subtitle: Text(context.l10n.settingsEnableBinSubtitle), + ), + ), const HiddenItemsTile(), if (device.canGrantDirectoryAccess) const StorageAccessTile(), ], diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index b55e3f81a..e5d1b610b 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -54,11 +54,11 @@ class _SettingsPageState extends State with FeedbackMixin { return [ PopupMenuItem( value: SettingsAction.export, - child: MenuRow(text: context.l10n.settingsActionExport, icon: const Icon(AIcons.export)), + child: MenuRow(text: context.l10n.settingsActionExport, icon: const Icon(AIcons.fileExport)), ), PopupMenuItem( value: SettingsAction.import, - child: MenuRow(text: context.l10n.settingsActionImport, icon: const Icon(AIcons.import)), + child: MenuRow(text: context.l10n.settingsActionImport, icon: const Icon(AIcons.fileImport)), ), ]; }, diff --git a/lib/widgets/settings/thumbnails/collection_actions_editor.dart b/lib/widgets/settings/thumbnails/collection_actions_editor.dart index 79c4594dc..a16a8977e 100644 --- a/lib/widgets/settings/thumbnails/collection_actions_editor.dart +++ b/lib/widgets/settings/thumbnails/collection_actions_editor.dart @@ -39,10 +39,10 @@ class CollectionActionEditorPage extends StatelessWidget { Tab(text: l10n.settingsCollectionQuickActionTabBrowsing), QuickActionEditorBody( bannerText: context.l10n.settingsCollectionBrowsingQuickActionEditorBanner, - allAvailableActions: EntrySetActions.browsing, + allAvailableActions: EntrySetActions.collectionEditorBrowsing, actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), - load: () => settings.collectionBrowsingQuickActions.toList(), + load: () => settings.collectionBrowsingQuickActions, save: (actions) => settings.collectionBrowsingQuickActions = actions, ), ), @@ -50,10 +50,10 @@ class CollectionActionEditorPage extends StatelessWidget { Tab(text: l10n.settingsCollectionQuickActionTabSelecting), QuickActionEditorBody( bannerText: context.l10n.settingsCollectionSelectionQuickActionEditorBanner, - allAvailableActions: EntrySetActions.selection, + allAvailableActions: EntrySetActions.collectionEditorSelection, actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), - load: () => settings.collectionSelectionQuickActions.toList(), + load: () => settings.collectionSelectionQuickActions, save: (actions) => settings.collectionSelectionQuickActions = actions, ), ), diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index 1ff395e94..4e5f00d4c 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -1,7 +1,7 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; @@ -26,7 +26,7 @@ class ThumbnailsSection extends StatelessWidget { return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.grid, - color: stringToColor('Thumbnails'), + color: AColors.thumbnails, ), title: context.l10n.settingsSectionThumbnails, expandedNotifier: expandedNotifier, diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index 52fa4b2b3..800ac3def 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -1,10 +1,9 @@ import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/video_loop_mode.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/settings/video_loop_mode.dart'; -import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -36,7 +35,7 @@ class VideoSection extends StatelessWidget { if (!standalonePage) SwitchListTile( value: currentShowVideos, - onChanged: (v) => context.read().changeFilterVisibility({MimeFilter.video}, v), + onChanged: (v) => settings.changeFilterVisibility({MimeFilter.video}, v), title: Text(context.l10n.settingsVideoShowVideos), ), const VideoActionsTile(), @@ -77,7 +76,7 @@ class VideoSection extends StatelessWidget { : AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.video, - color: stringToColor('Video'), + color: AColors.video, ), title: context.l10n.settingsSectionVideo, expandedNotifier: expandedNotifier, diff --git a/lib/widgets/settings/video/video_actions_editor.dart b/lib/widgets/settings/video/video_actions_editor.dart index d0429ffa2..f52cd5ab4 100644 --- a/lib/widgets/settings/video/video_actions_editor.dart +++ b/lib/widgets/settings/video/video_actions_editor.dart @@ -37,7 +37,7 @@ class VideoActionEditorPage extends StatelessWidget { allAvailableActions: VideoActions.all, actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), - load: () => settings.videoQuickActions.toList(), + load: () => settings.videoQuickActions, save: (actions) => settings.videoQuickActions = actions, ); } diff --git a/lib/widgets/settings/viewer/entry_background.dart b/lib/widgets/settings/viewer/entry_background.dart index 23a9017e0..2786129bd 100644 --- a/lib/widgets/settings/viewer/entry_background.dart +++ b/lib/widgets/settings/viewer/entry_background.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/settings/entry_background.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/entry_background.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index 3e0ddb026..3b684621a 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -1,8 +1,8 @@ -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; @@ -25,7 +25,7 @@ class ViewerSection extends StatelessWidget { return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.image, - color: stringToColor('Image'), + color: AColors.image, ), title: context.l10n.settingsSectionViewer, expandedNotifier: expandedNotifier, diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index 80e59c7f4..88b1859d6 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -30,16 +30,7 @@ class ViewerActionEditorPage extends StatelessWidget { const ViewerActionEditorPage({Key? key}) : super(key: key); static const allAvailableActions = [ - EntryAction.info, - EntryAction.toggleFavourite, - EntryAction.share, - EntryAction.delete, - EntryAction.rename, - EntryAction.export, - EntryAction.addShortcut, - EntryAction.copyToClipboard, - EntryAction.print, - EntryAction.rotateScreen, + ...EntryActions.topLevel, EntryAction.rotateCCW, EntryAction.rotateCW, EntryAction.flip, @@ -53,7 +44,7 @@ class ViewerActionEditorPage extends StatelessWidget { allAvailableActions: allAvailableActions, actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), - load: () => settings.viewerQuickActions.toList(), + load: () => settings.viewerQuickActions, save: (actions) => settings.viewerQuickActions = actions, ); } diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index ea05acb8a..635b91e05 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -1,5 +1,4 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -41,6 +40,7 @@ class FilterTable extends StatelessWidget { final textScaleFactor = MediaQuery.textScaleFactorOf(context); final lineHeight = 16 * textScaleFactor; + final barRadius = Radius.circular(lineHeight / 2); final isRtl = context.isRtl; return Padding( @@ -52,7 +52,6 @@ class FilterTable extends StatelessWidget { return Table( children: displayedEntries.map((kv) { final filter = filterBuilder(kv.key); - final label = filter.getLabel(context); final count = kv.value; final percent = count / totalEntryCount; return TableRow( @@ -69,17 +68,32 @@ class FilterTable extends StatelessWidget { ), ), if (showPercentIndicator) - LinearPercentIndicator( - percent: percent, - lineHeight: lineHeight, - backgroundColor: Colors.white24, - progressColor: stringToColor(label), - animation: true, - isRTL: isRtl, + // as of percent_indicator v4.0.0, bar radius is not correctly applied to progress bar + // when width is lower than height, so we clip it and handle padding outside + Padding( padding: EdgeInsets.symmetric(horizontal: lineHeight), - center: Text( - intl.NumberFormat.percentPattern().format(percent), - style: const TextStyle(shadows: Constants.embossShadows), + child: ClipRRect( + borderRadius: BorderRadius.all(barRadius), + child: FutureBuilder( + future: filter.color(context), + builder: (context, snapshot) { + final color = snapshot.data; + return LinearPercentIndicator( + percent: percent, + lineHeight: lineHeight, + backgroundColor: Colors.white24, + progressColor: color, + animation: true, + isRTL: isRtl, + barRadius: barRadius, + center: Text( + intl.NumberFormat.percentPattern().format(percent), + style: const TextStyle(shadows: Constants.embossShadows), + ), + padding: EdgeInsets.zero, + ); + }, + ), ), ), Text( diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index c94663d50..32db49dae 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -6,7 +6,7 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/settings/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -96,27 +96,40 @@ class StatsPage extends StatelessWidget { final withGpsPercent = withGpsCount / entries.length; final textScaleFactor = MediaQuery.textScaleFactorOf(context); final lineHeight = 16 * textScaleFactor; + final barRadius = Radius.circular(lineHeight / 2); final locationIndicator = Padding( padding: const EdgeInsets.all(16), child: Column( children: [ - Padding( - // end padding to match leading, so that inside label is aligned with outside label below - padding: const EdgeInsetsDirectional.only(end: 24), - child: LinearPercentIndicator( - percent: withGpsPercent, - lineHeight: lineHeight, - backgroundColor: Colors.white24, - progressColor: Theme.of(context).colorScheme.secondary, - animation: animate, - isRTL: context.isRtl, - leading: const Icon(AIcons.location), - padding: EdgeInsets.symmetric(horizontal: lineHeight), - center: Text( - intl.NumberFormat.percentPattern().format(withGpsPercent), - style: const TextStyle(shadows: Constants.embossShadows), + // as of percent_indicator v4.0.0, bar radius is not correctly applied to progress bar + // when width is lower than height, so we clip it and handle padding outside + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(AIcons.location), + SizedBox(width: lineHeight), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.all(barRadius), + child: LinearPercentIndicator( + percent: withGpsPercent, + lineHeight: lineHeight, + backgroundColor: Colors.white24, + progressColor: Theme.of(context).colorScheme.secondary, + animation: animate, + isRTL: context.isRtl, + barRadius: barRadius, + center: Text( + intl.NumberFormat.percentPattern().format(withGpsPercent), + style: const TextStyle(shadows: Constants.embossShadows), + ), + padding: EdgeInsets.zero, + ), + ), ), - ), + // end padding to match leading, so that inside label is aligned with outside label below + SizedBox(width: lineHeight + 24), + ], ), const SizedBox(height: 8), Text( diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index f0e1aab86..ab151bec2 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -9,6 +9,8 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/highlight.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/image_op_events.dart'; @@ -17,11 +19,13 @@ import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/media_file_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/entry_editors/rename_dialog.dart'; import 'package:aves/widgets/dialogs/export_entry_dialog.dart'; @@ -36,7 +40,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, SingleEntryEditorMixin { +class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, SingleEntryEditorMixin, EntryStorageMixin { @override final AvesEntry entry; @@ -55,11 +59,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.delete: _delete(context); break; - case EntryAction.export: - _export(context); + case EntryAction.restore: + _move(context, moveType: MoveType.fromBin); break; - case EntryAction.info: - ShowInfoNotification().dispatch(context); + case EntryAction.convert: + _convert(context); break; case EntryAction.print: EntryPrinter(entry).print(context); @@ -67,6 +71,12 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.rename: _rename(context); break; + case EntryAction.copy: + _move(context, moveType: MoveType.copy); + break; + case EntryAction.move: + _move(context, moveType: MoveType.move); + break; case EntryAction.share: androidAppService.shareEntries({entry}).then((success) { if (!success) showNoMatchingAppDialog(context); @@ -159,44 +169,42 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } Future _delete(BuildContext context) async { - final confirmed = await showDialog( + if (settings.enableBin && !entry.trashed) { + await _move(context, moveType: MoveType.toBin); + return; + } + + final l10n = context.l10n; + if (!(await showConfirmationDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(1)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.deleteButtonLabel), - ), - ], - ); - }, - ); - if (confirmed == null || !confirmed) return; + type: ConfirmationDialog.delete, + message: l10n.deleteEntriesConfirmationDialogMessage(1), + confirmationButtonLabel: l10n.deleteButtonLabel, + ))) return; if (!await checkStoragePermission(context, {entry})) return; if (!await entry.delete()) { - showFeedback(context, context.l10n.genericFailureFeedback); + showFeedback(context, l10n.genericFailureFeedback); } else { final source = context.read(); - if (source.initialized) { - await source.removeEntries({entry.uri}); + if (source.initState != SourceInitializationState.none) { + await source.removeEntries({entry.uri}, includeTrash: true); } - EntryDeletedNotification(entry).dispatch(context); + EntryRemovedNotification(entry).dispatch(context); } } - Future _export(BuildContext context) async { + Future _convert(BuildContext context) async { + final options = await showDialog( + context: context, + builder: (context) => ExportEntryDialog(entry: entry), + ); + if (options == null) return; + final source = context.read(); - if (!source.initialized) { + if (source.initState != SourceInitializationState.full) { await source.init(); - unawaited(source.refresh()); } final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export); if (destinationAlbum == null) return; @@ -204,12 +212,6 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return; - final options = await showDialog( - context: context, - builder: (context) => ExportEntryDialog(entry: entry), - ); - if (options == null) return; - final selection = {}; if (entry.isMultiPage) { final multiPageInfo = await entry.getMultiPageInfo(); @@ -227,9 +229,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final selectionCount = selection.length; source.pauseMonitoring(); - showOpReport( + await showOpReport( context: context, - // TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures) opStream: mediaFileService.export( selection, options: options, @@ -293,6 +294,21 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix ); } + Future _move(BuildContext context, {required MoveType moveType}) async { + await move( + context, + moveType: moveType, + entries: {entry}, + onSuccess: { + MoveType.move, + MoveType.toBin, + MoveType.fromBin, + }.contains(moveType) + ? () => EntryRemovedNotification(entry).dispatch(context) + : null, + ); + } + Future _rename(BuildContext context) async { final newName = await showDialog( context: context, diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 77007ede6..727065f34 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -9,7 +9,9 @@ import 'package:aves/widgets/common/action_mixins/entry_editor.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/viewer/action/single_entry_editor.dart'; +import 'package:aves/widgets/viewer/debug/debug_page.dart'; import 'package:aves/widgets/viewer/embedded/notifications.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEditorMixin, SingleEntryEditorMixin { @@ -35,6 +37,9 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi // motion photo case EntryInfoAction.viewMotionPhotoVideo: return entry.isMotionPhoto; + // debug + case EntryInfoAction.debug: + return kDebugMode; } } @@ -54,6 +59,9 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi // motion photo case EntryInfoAction.viewMotionPhotoVideo: return true; + // debug + case EntryInfoAction.debug: + return true; } } @@ -80,6 +88,10 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi case EntryInfoAction.viewMotionPhotoVideo: OpenEmbeddedDataNotification.motionPhotoVideo().dispatch(context); break; + // debug + case EntryInfoAction.debug: + _goToDebug(context); + break; } _eventStreamController.add(ActionEndedEvent(action)); } @@ -122,4 +134,14 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi await edit(context, () => entry.removeMetadata(types)); } + + void _goToDebug(BuildContext context) { + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: ViewerDebugPage.routeName), + builder: (context) => ViewerDebugPage(entry: entry), + ), + ); + } } diff --git a/lib/widgets/viewer/action/printer.dart b/lib/widgets/viewer/action/printer.dart index 9db7aec24..8f2526db6 100644 --- a/lib/widgets/viewer/action/printer.dart +++ b/lib/widgets/viewer/action/printer.dart @@ -52,11 +52,11 @@ class EntryPrinter with FeedbackMixin { final pageCount = multiPageInfo.pageCount; if (pageCount > 1) { final streamController = StreamController.broadcast(); - showOpReport( + unawaited(showOpReport( context: context, opStream: streamController.stream, itemCount: pageCount, - ); + )); for (var page = 0; page < pageCount; page++) { final pageEntry = multiPageInfo.getPageEntryByIndex(page); _addPdfPage(await _buildPageImage(pageEntry)); diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index ecc490ee5..d94af86ee 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; +import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/video_playback.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/viewer/info/common.dart'; @@ -24,6 +25,7 @@ class _DbTabState extends State { late Future _dbEntryLoader; late Future _dbMetadataLoader; late Future _dbAddressLoader; + late Future _dbTrashDetailsLoader; late Future _dbVideoPlaybackLoader; AvesEntry get entry => widget.entry; @@ -35,12 +37,13 @@ class _DbTabState extends State { } void _loadDatabase() { - final contentId = entry.contentId; - _dbDateLoader = metadataDb.loadDates().then((values) => values[contentId]); - _dbEntryLoader = metadataDb.loadAllEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); - _dbMetadataLoader = metadataDb.loadAllMetadataEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); - _dbAddressLoader = metadataDb.loadAllAddresses().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); - _dbVideoPlaybackLoader = metadataDb.loadVideoPlayback(contentId); + final id = entry.id; + _dbDateLoader = metadataDb.loadDates().then((values) => values[id]); + _dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhereOrNull((row) => row.id == id)); + _dbMetadataLoader = metadataDb.loadCatalogMetadata().then((values) => values.firstWhereOrNull((row) => row.id == id)); + _dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.id == id)); + _dbTrashDetailsLoader = metadataDb.loadAllTrashDetails().then((values) => values.firstWhereOrNull((row) => row.id == id)); + _dbVideoPlaybackLoader = metadataDb.loadVideoPlayback(id); setState(() {}); } @@ -94,6 +97,7 @@ class _DbTabState extends State { 'dateModifiedSecs': '${data.dateModifiedSecs}', 'sourceDateTakenMillis': '${data.sourceDateTakenMillis}', 'durationMillis': '${data.durationMillis}', + 'trashed': '${data.trashed}', }, ), ], @@ -155,6 +159,28 @@ class _DbTabState extends State { }, ), const SizedBox(height: 16), + FutureBuilder( + future: _dbTrashDetailsLoader, + builder: (context, snapshot) { + if (snapshot.hasError) return Text(snapshot.error.toString()); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); + final data = snapshot.data; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('DB trash details:${data == null ? ' no row' : ''}'), + if (data != null) + InfoRowGroup( + info: { + 'dateMillis': '${data.dateMillis}', + 'path': data.path, + }, + ), + ], + ); + }, + ), + const SizedBox(height: 16), FutureBuilder( future: _dbVideoPlaybackLoader, builder: (context, snapshot) { diff --git a/lib/widgets/viewer/debug/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart index f916b96d5..7fe35f4a7 100644 --- a/lib/widgets/viewer/debug/debug_page.dart +++ b/lib/widgets/viewer/debug/debug_page.dart @@ -68,8 +68,9 @@ class ViewerDebugPage extends StatelessWidget { InfoRowGroup( info: { 'hash': '#${shortHash(entry)}', - 'uri': entry.uri, + 'id': '${entry.id}', 'contentId': '${entry.contentId}', + 'uri': entry.uri, 'path': entry.path ?? '', 'directory': entry.directory ?? '', 'filenameWithoutExtension': entry.filenameWithoutExtension ?? '', @@ -77,6 +78,7 @@ class ViewerDebugPage extends StatelessWidget { 'sourceTitle': entry.sourceTitle ?? '', 'sourceMimeType': entry.sourceMimeType, 'mimeType': entry.mimeType, + 'trashed': '${entry.trashed}', 'isMissingAtPath': '${entry.isMissingAtPath}', }, ), diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index 86eddea63..a2dc6463d 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -1,5 +1,5 @@ import 'package:aves/model/entry.dart'; -import 'package:aves/model/settings/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart'; diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 299ea5bf2..d1d4e28f6 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -1,10 +1,11 @@ import 'dart:math'; +import 'package:aves/app_mode.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/common/services.dart'; @@ -97,7 +98,7 @@ class _EntryViewerStackState extends State with FeedbackMixin, // so it is, strictly speaking, not contained in the lens used by the viewer, // but it can be found by content ID final initialEntry = widget.initialEntry; - final entry = entries.firstWhereOrNull((v) => v.contentId == initialEntry.contentId) ?? entries.firstOrNull; + final entry = entries.firstWhereOrNull((entry) => entry.id == initialEntry.id) ?? entries.firstOrNull; // opening hero, with viewer as target _heroInfoNotifier.value = HeroInfo(collection?.id, entry); _entryNotifier.value = entry; @@ -195,8 +196,8 @@ class _EntryViewerStackState extends State with FeedbackMixin, onNotification: (dynamic notification) { if (notification is FilterSelectedNotification) { _goToCollection(notification.filter); - } else if (notification is EntryDeletedNotification) { - _onEntryDeleted(context, notification.entry); + } else if (notification is EntryRemovedNotification) { + _onEntryRemoved(context, notification.entry); } return false; }, @@ -399,8 +400,12 @@ class _EntryViewerStackState extends State with FeedbackMixin, } void _goToCollection(CollectionFilter filter) { + final isMainMode = context.read>().value == AppMode.main; + if (!isMainMode) return; + final baseCollection = collection; if (baseCollection == null) return; + _onLeave(); Navigator.pushAndRemoveUntil( context, @@ -453,7 +458,8 @@ class _EntryViewerStackState extends State with FeedbackMixin, _updateEntry(); } - void _onEntryDeleted(BuildContext context, AvesEntry entry) { + void _onEntryRemoved(BuildContext context, AvesEntry entry) { + // deleted or moved to another album if (hasCollection) { final entries = collection!.sortedEntries; entries.remove(entry); diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index a750d91b7..91b3a97a3 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -70,11 +70,11 @@ class BasicSection extends StatelessWidget { if (entry.isVideo) ..._buildVideoRows(context), if (showResolution) l10n.viewerInfoLabelResolution: rasterResolutionText, l10n.viewerInfoLabelSize: sizeText, - l10n.viewerInfoLabelUri: entry.uri, + if (!entry.trashed) l10n.viewerInfoLabelUri: entry.uri, if (path != null) l10n.viewerInfoLabelPath: path, }, ), - OwnerProp(entry: entry), + if (!entry.trashed) OwnerProp(entry: entry), _buildChips(context), _buildEditButtons(context), ], diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 0b4185886..beb758122 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -9,6 +9,7 @@ import 'package:aves/widgets/common/sliver_app_bar_title.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/info/info_search.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -53,7 +54,13 @@ class InfoAppBar extends StatelessWidget { if (entry.canEdit) MenuIconTheme( child: PopupMenuButton( - itemBuilder: (context) => menuActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(action))).toList(), + itemBuilder: (context) => [ + ...menuActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(action))), + if (!kReleaseMode) ...[ + const PopupMenuDivider(), + _toMenuItem(context, EntryInfoAction.debug, enabled: true), + ] + ], onSelected: (action) async { // wait for the popup menu to hide before proceeding with the action await Future.delayed(Durations.popupMenuAnimation * timeDilation); diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 2f370f5f3..d6036f936 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -198,7 +198,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { collection: collection, actionDelegate: _actionDelegate, isEditingMetadataNotifier: _isEditingMetadataNotifier, - onFilter: _goToCollection, + onFilter: _onFilter, ); final locationAtTop = widget.split && entry.hasGps; final locationSection = LocationSection( @@ -206,7 +206,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { entry: entry, showTitle: !locationAtTop, isScrollingNotifier: widget.isScrollingNotifier, - onFilter: _goToCollection, + onFilter: _onFilter, ); final basicAndLocationSliver = locationAtTop ? SliverToBoxAdapter( @@ -264,8 +264,5 @@ class _InfoPageContentState extends State<_InfoPageContent> { }); } - void _goToCollection(CollectionFilter filter) { - if (collection == null) return; - FilterSelectedNotification(filter).dispatch(context); - } + void _onFilter(CollectionFilter filter) => FilterSelectedNotification(filter).dispatch(context); } diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index d216417ff..767e0dd8b 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -1,6 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/location.dart'; -import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/common/services.dart'; diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 01dd5a393..44b4ae3e9 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'package:aves/model/entry.dart'; -import 'package:aves/utils/color_utils.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/utils/xmp_utils.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; @@ -43,7 +43,7 @@ class _XmpDirTileState extends State { return AvesExpansionTile( // title may contain parent to distinguish multiple XMP directories title: widget.title, - color: stringToColor('XMP'), + color: AColors.xmp, expandedNotifier: widget.expandedNotifier, initiallyExpanded: widget.initiallyExpanded, children: [ diff --git a/lib/widgets/viewer/info/notifications.dart b/lib/widgets/viewer/info/notifications.dart index 1e97fafa8..e46ced611 100644 --- a/lib/widgets/viewer/info/notifications.dart +++ b/lib/widgets/viewer/info/notifications.dart @@ -12,8 +12,9 @@ class FilterSelectedNotification extends Notification { const FilterSelectedNotification(this.filter); } -class EntryDeletedNotification extends Notification { +// deleted or moved to another album +class EntryRemovedNotification extends Notification { final AvesEntry entry; - const EntryDeletedNotification(this.entry); + const EntryRemovedNotification(this.entry); } diff --git a/lib/widgets/viewer/overlay/bottom/common.dart b/lib/widgets/viewer/overlay/bottom/common.dart index f30eda557..775c418dd 100644 --- a/lib/widgets/viewer/overlay/bottom/common.dart +++ b/lib/widgets/viewer/overlay/bottom/common.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata/overlay.dart'; import 'package:aves/model/multipage.dart'; -import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; @@ -384,7 +384,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 ?? entry.contentId) ?? multiPageInfo.defaultPage; + final page = multiPageInfo.getById(entry.pageId ?? entry.id) ?? multiPageInfo.defaultPage; pagePosition = '${(page?.index ?? 0) + 1}/$pageCount'; } } diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video.dart index 7e453e924..564325bd5 100644 --- a/lib/widgets/viewer/overlay/bottom/video.dart +++ b/lib/widgets/viewer/overlay/bottom/video.dart @@ -67,14 +67,15 @@ class _VideoControlOverlayState extends State with SingleTi final status = controller?.status ?? VideoStatus.idle; Widget child; if (status == VideoStatus.error) { + const action = VideoAction.playOutside; child = Align( alignment: AlignmentDirectional.centerEnd, child: OverlayButton( scale: scale, child: IconButton( - icon: VideoAction.playOutside.getIcon(), - onPressed: () => widget.onActionSelected(VideoAction.playOutside), - tooltip: VideoAction.playOutside.getText(context), + icon: action.getIcon(), + onPressed: entry.trashed ? null : () => widget.onActionSelected(action), + tooltip: action.getText(context), ), ), ); @@ -327,13 +328,15 @@ class _ButtonRow extends StatelessWidget { case VideoAction.setSpeed: enabled = controller?.canSetSpeedNotifier.value ?? false; break; - case VideoAction.playOutside: case VideoAction.replay10: case VideoAction.skip10: case VideoAction.settings: case VideoAction.togglePlay: enabled = true; break; + case VideoAction.playOutside: + enabled = !(controller?.entry.trashed ?? true); + break; } Widget? child; diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index 3a30626ff..42a5617ab 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -78,6 +78,8 @@ class MinimapPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + if (entrySize.width <= 0 || entrySize.height <= 0) return; + final viewSize = entrySize * viewScale; if (viewSize.isEmpty) return; diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 846f258d5..dcd2d774a 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -3,8 +3,10 @@ import 'package:aves/model/device.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'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup_menu_button.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/favourite_toggler.dart'; import 'package:aves/widgets/viewer/action/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; @@ -13,6 +15,7 @@ import 'package:aves/widgets/viewer/overlay/minimap.dart'; import 'package:aves/widgets/viewer/overlay/notifications.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/visual/conductor.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -62,52 +65,68 @@ class ViewerTopOverlay extends StatelessWidget { Widget _buildOverlay(BuildContext context, int availableCount, AvesEntry mainEntry, {AvesEntry? pageEntry}) { pageEntry ??= mainEntry; + final trashed = mainEntry.trashed; bool _isVisible(EntryAction action) { - final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry! : mainEntry; - switch (action) { - case EntryAction.toggleFavourite: - return canToggleFavourite; - case EntryAction.delete: - case EntryAction.rename: - return targetEntry.canEdit; - case EntryAction.rotateCCW: - case EntryAction.rotateCW: - case EntryAction.flip: - return targetEntry.canRotateAndFlip; - case EntryAction.export: - case EntryAction.print: - return !targetEntry.isVideo && device.canPrint; - case EntryAction.openMap: - return targetEntry.hasGps; - case EntryAction.viewSource: - return targetEntry.isSvg; - case EntryAction.rotateScreen: - return settings.isRotationLocked; - case EntryAction.addShortcut: - return device.canPinShortcut; - case EntryAction.copyToClipboard: - case EntryAction.edit: - case EntryAction.info: - case EntryAction.open: - case EntryAction.setAs: - case EntryAction.share: - return true; - case EntryAction.debug: - return kDebugMode; + if (trashed) { + switch (action) { + case EntryAction.delete: + case EntryAction.restore: + return true; + case EntryAction.debug: + return kDebugMode; + default: + return false; + } + } else { + final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry! : mainEntry; + switch (action) { + case EntryAction.toggleFavourite: + return canToggleFavourite; + case EntryAction.delete: + case EntryAction.rename: + case EntryAction.copy: + case EntryAction.move: + return targetEntry.canEdit; + case EntryAction.rotateCCW: + case EntryAction.rotateCW: + case EntryAction.flip: + return targetEntry.canRotateAndFlip; + case EntryAction.convert: + case EntryAction.print: + return !targetEntry.isVideo && device.canPrint; + case EntryAction.openMap: + return targetEntry.hasGps; + case EntryAction.viewSource: + return targetEntry.isSvg; + case EntryAction.rotateScreen: + return settings.isRotationLocked; + case EntryAction.addShortcut: + return device.canPinShortcut; + case EntryAction.copyToClipboard: + case EntryAction.edit: + case EntryAction.open: + case EntryAction.setAs: + case EntryAction.share: + return true; + case EntryAction.restore: + return false; + case EntryAction.debug: + return kDebugMode; + } } } final buttonRow = Selector( selector: (context, s) => s.isRotationLocked, builder: (context, s, child) { - final quickActions = settings.viewerQuickActions.where(_isVisible).take(availableCount - 1).toList(); - final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_isVisible).toList(); - final externalAppActions = EntryActions.externalApp.where(_isVisible).toList(); + final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(_isVisible).take(availableCount - 1).toList(); + final topLevelActions = EntryActions.topLevel.where((action) => !quickActions.contains(action)).where(_isVisible).toList(); + final exportActions = EntryActions.export.where((action) => !quickActions.contains(action)).where(_isVisible).toList(); return _TopOverlayRow( quickActions: quickActions, - inAppActions: inAppActions, - externalAppActions: externalAppActions, + topLevelActions: topLevelActions, + exportActions: exportActions, scale: scale, mainEntry: mainEntry, pageEntry: pageEntry!, @@ -138,7 +157,7 @@ class ViewerTopOverlay extends StatelessWidget { } class _TopOverlayRow extends StatelessWidget { - final List quickActions, inAppActions, externalAppActions; + final List quickActions, topLevelActions, exportActions; final Animation scale; final AvesEntry mainEntry, pageEntry; @@ -147,8 +166,8 @@ class _TopOverlayRow extends StatelessWidget { const _TopOverlayRow({ Key? key, required this.quickActions, - required this.inAppActions, - required this.externalAppActions, + required this.topLevelActions, + required this.exportActions, required this.scale, required this.mainEntry, required this.pageEntry, @@ -156,6 +175,7 @@ class _TopOverlayRow extends StatelessWidget { @override Widget build(BuildContext context) { + final hasOverflowMenu = pageEntry.canRotateAndFlip || topLevelActions.isNotEmpty || exportActions.isNotEmpty; return Row( children: [ OverlayButton( @@ -164,34 +184,50 @@ class _TopOverlayRow extends StatelessWidget { ), const Spacer(), ...quickActions.map((action) => _buildOverlayButton(context, action)), - OverlayButton( - scale: scale, - child: MenuIconTheme( - child: AvesPopupMenuButton( - key: const Key('entry-menu-button'), - itemBuilder: (context) => [ - ...inAppActions.map((action) => _buildPopupMenuItem(context, action)), - if (pageEntry.canRotateAndFlip) _buildRotateAndFlipMenuItems(context), - const PopupMenuDivider(), - ...externalAppActions.map((action) => _buildPopupMenuItem(context, action)), - if (!kReleaseMode) ...[ - const PopupMenuDivider(), - _buildPopupMenuItem(context, EntryAction.debug), - ] - ], - onSelected: (action) { - // wait for the popup menu to hide before proceeding with the action - Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onActionSelected(context, action)); - }, - onMenuOpened: () { - // if the menu is opened while overlay is hiding, - // the popup menu button is disposed and menu items are ineffective, - // so we make sure overlay stays visible - const ToggleOverlayNotification(visible: true).dispatch(context); - }, + if (hasOverflowMenu) + OverlayButton( + scale: scale, + child: MenuIconTheme( + child: AvesPopupMenuButton( + key: const Key('entry-menu-button'), + itemBuilder: (context) { + final exportInternalActions = exportActions.whereNot(EntryActions.exportExternal.contains).toList(); + final exportExternalActions = exportActions.where(EntryActions.exportExternal.contains).toList(); + return [ + if (pageEntry.canRotateAndFlip) _buildRotateAndFlipMenuItems(context), + ...topLevelActions.map((action) => _buildPopupMenuItem(context, action)), + if (exportActions.isNotEmpty) + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupMenuItemExpansionPanel( + icon: AIcons.export, + title: context.l10n.entryActionExport, + items: [ + ...exportInternalActions.map((action) => _buildPopupMenuItem(context, action)).toList(), + if (exportInternalActions.isNotEmpty && exportExternalActions.isNotEmpty) const PopupMenuDivider(height: 0), + ...exportExternalActions.map((action) => _buildPopupMenuItem(context, action)).toList(), + ], + ), + ), + if (!kReleaseMode) ...[ + const PopupMenuDivider(), + _buildPopupMenuItem(context, EntryAction.debug), + ] + ]; + }, + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onActionSelected(context, action)); + }, + onMenuOpened: () { + // if the menu is opened while overlay is hiding, + // the popup menu button is disposed and menu items are ineffective, + // so we make sure overlay stays visible + const ToggleOverlayNotification(visible: true).dispatch(context); + }, + ), ), ), - ), ], ); } @@ -206,44 +242,24 @@ class _TopOverlayRow extends StatelessWidget { onPressed: onPressed, ); break; - case EntryAction.addShortcut: - case EntryAction.copyToClipboard: - 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.share: - case EntryAction.rotateScreen: - case EntryAction.viewSource: + default: child = IconButton( - icon: action.getIcon() ?? const SizedBox(), + icon: action.getIcon(), onPressed: onPressed, tooltip: action.getText(context), ); break; - case EntryAction.openMap: - case EntryAction.open: - case EntryAction.edit: - case EntryAction.setAs: - case EntryAction.debug: - break; } - return child != null - ? Padding( - padding: const EdgeInsetsDirectional.only(end: ViewerTopOverlay.innerPadding), - child: OverlayButton( - scale: scale, - child: child, - ), - ) - : const SizedBox.shrink(); + return Padding( + padding: const EdgeInsetsDirectional.only(end: ViewerTopOverlay.innerPadding), + child: OverlayButton( + scale: scale, + child: child, + ), + ); } - PopupMenuEntry _buildPopupMenuItem(BuildContext context, EntryAction action) { + PopupMenuItem _buildPopupMenuItem(BuildContext context, EntryAction action) { Widget? child; switch (action) { // in app actions diff --git a/lib/widgets/viewer/video/controller.dart b/lib/widgets/viewer/video/controller.dart index fdeff47bf..58048fefa 100644 --- a/lib/widgets/viewer/video/controller.dart +++ b/lib/widgets/viewer/video/controller.dart @@ -27,36 +27,34 @@ abstract class AvesVideoController { } Future _savePlaybackState() async { - final contentId = entry.contentId; - if (contentId == null || !isReady || duration < resumeTimeSaveMinDuration.inMilliseconds) return; + final id = entry.id; + if (!isReady || duration < resumeTimeSaveMinDuration.inMilliseconds) return; if (persistPlayback) { final _progress = progress; if (resumeTimeSaveMinProgress < _progress && _progress < resumeTimeSaveMaxProgress) { await metadataDb.addVideoPlayback({ VideoPlaybackRow( - contentId: contentId, + entryId: id, resumeTimeMillis: currentPosition, ) }); } else { - await metadataDb.removeVideoPlayback({contentId}); + await metadataDb.removeVideoPlayback({id}); } } } Future getResumeTime(BuildContext context) async { - final contentId = entry.contentId; - if (contentId == null) return null; - if (!persistPlayback) return null; - final playback = await metadataDb.loadVideoPlayback(contentId); + final id = entry.id; + final playback = await metadataDb.loadVideoPlayback(id); final resumeTime = playback?.resumeTimeMillis ?? 0; if (resumeTime == 0) return null; // clear on retrieval - await metadataDb.removeVideoPlayback({contentId}); + await metadataDb.removeVideoPlayback({id}); final resume = await showDialog( context: context, diff --git a/lib/widgets/viewer/video/fijkplayer.dart b/lib/widgets/viewer/video/fijkplayer.dart index f04f0f164..24c5e7bf2 100644 --- a/lib/widgets/viewer/video/fijkplayer.dart +++ b/lib/widgets/viewer/video/fijkplayer.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/settings/video_loop_mode.dart'; +import 'package:aves/model/settings/enums/video_loop_mode.dart'; import 'package:aves/model/video/keys.dart'; import 'package:aves/model/video/metadata.dart'; import 'package:aves/utils/change_notifier.dart'; diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 38a05b853..20ad5e563 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/settings/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/magnifier/controller/controller.dart'; @@ -143,7 +143,7 @@ class _EntryPageViewState extends State { if (animate) { child = Consumer( builder: (context, info, child) => Hero( - tag: info != null && info.entry == mainEntry ? Object.hashAll([info.collectionId, mainEntry.uri]) : hashCode, + tag: info != null && info.entry == mainEntry ? Object.hashAll([info.collectionId, mainEntry.id]) : hashCode, transitionOnUserGestures: true, child: child!, ), diff --git a/lib/widgets/viewer/visual/error.dart b/lib/widgets/viewer/visual/error.dart index e233637e7..e53ecc716 100644 --- a/lib/widgets/viewer/visual/error.dart +++ b/lib/widgets/viewer/visual/error.dart @@ -49,6 +49,7 @@ class _ErrorViewState extends State { icon: exists ? AIcons.error : AIcons.broken, text: exists ? context.l10n.viewerErrorUnknown : context.l10n.viewerErrorDoesNotExist, alignment: Alignment.center, + safeBottom: false, ); }), ), diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 5e1ac19f3..f15a7df16 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -3,8 +3,8 @@ import 'dart:math'; import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/settings/entry_background.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/entry_background.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; @@ -350,6 +350,7 @@ class _RegionTile extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); + properties.add(IntProperty('id', entry.id)); properties.add(IntProperty('contentId', entry.contentId)); properties.add(DiagnosticsProperty('tileRect', tileRect)); properties.add(DiagnosticsProperty>('regionRect', regionRect)); diff --git a/lib/widgets/viewer/visual/state.dart b/lib/widgets/viewer/visual/state.dart index efae82e5e..416400db2 100644 --- a/lib/widgets/viewer/visual/state.dart +++ b/lib/widgets/viewer/visual/state.dart @@ -1,15 +1,16 @@ -import 'package:flutter/foundation.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; -class ViewState { +@immutable +class ViewState extends Equatable { final Offset position; final double? scale; final Size? viewportSize; static const ViewState zero = ViewState(Offset.zero, 0, null); - const ViewState(this.position, this.scale, this.viewportSize); - @override - String toString() => '$runtimeType#${shortHash(this)}{position=$position, scale=$scale, viewportSize=$viewportSize}'; + List get props => [position, scale, viewportSize]; + + const ViewState(this.position, this.scale, this.viewportSize); } diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index a2ae34d07..dc41b5708 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -4,8 +4,8 @@ import 'dart:ui'; import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/settings/entry_background.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/entry_background.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; @@ -305,6 +305,7 @@ class _RegionTile extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); + properties.add(IntProperty('id', entry.id)); properties.add(IntProperty('contentId', entry.contentId)); properties.add(DiagnosticsProperty('tileRect', tileRect)); properties.add(DiagnosticsProperty>('regionRect', regionRect)); diff --git a/pubspec.lock b/pubspec.lock index 52bb31a05..bc228cb42 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -224,14 +224,14 @@ packages: name: device_info_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.0+1" device_info_plus_web: dependency: transitive description: @@ -305,35 +305,35 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "1.12.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.2.3" + version: "4.2.4" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "1.5.4" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "2.4.5" + version: "2.5.1" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.1.12" + version: "3.1.13" flex_color_picker: dependency: "direct main" description: @@ -567,6 +567,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" material_design_icons_flutter: dependency: "direct main" description: @@ -615,7 +622,7 @@ packages: name: nm url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.4" node_preamble: dependency: transitive description: @@ -734,21 +741,14 @@ packages: name: pdf url: "https://pub.dartlang.org" source: hosted - version: "3.6.5" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" + version: "3.7.1" percent_indicator: dependency: "direct main" description: name: percent_indicator url: "https://pub.dartlang.org" source: hosted - version: "3.4.0" + version: "4.0.0" permission_handler: dependency: "direct main" description: @@ -776,7 +776,7 @@ packages: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: @@ -804,7 +804,7 @@ packages: name: printing url: "https://pub.dartlang.org" source: hosted - version: "5.6.6" + version: "5.7.2" process: dependency: transitive description: @@ -853,37 +853,37 @@ packages: name: screen_brightness url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" screen_brightness_android: dependency: transitive description: name: screen_brightness_android url: "https://pub.dartlang.org" source: hosted - version: "0.0.3" + version: "0.0.4" screen_brightness_ios: dependency: transitive description: name: screen_brightness_ios url: "https://pub.dartlang.org" source: hosted - version: "0.0.4" + version: "0.0.5" screen_brightness_platform_interface: dependency: transitive description: name: screen_brightness_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.0.3" + version: "0.0.4" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" + version: "2.0.13" shared_preferences_android: - dependency: transitive + dependency: "direct main" description: name: shared_preferences_android url: "https://pub.dartlang.org" @@ -895,21 +895,21 @@ packages: name: shared_preferences_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.10" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.1.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_platform_interface: dependency: transitive description: @@ -930,7 +930,7 @@ packages: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.1.0" shelf: dependency: transitive description: @@ -1063,21 +1063,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.17.12" + version: "1.19.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.8" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.9" transparent_image: dependency: "direct main" description: @@ -1119,28 +1119,28 @@ packages: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.14" + version: "6.0.15" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.14" + version: "6.0.15" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" url_launcher_platform_interface: dependency: transitive description: @@ -1154,7 +1154,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.8" url_launcher_windows: dependency: transitive description: @@ -1175,7 +1175,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "7.3.0" + version: "7.5.0" watcher: dependency: transitive description: @@ -1210,7 +1210,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.3" + version: "2.3.11" wkt_parser: dependency: transitive description: @@ -1224,7 +1224,7 @@ packages: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.0+1" xml: dependency: "direct main" description: @@ -1240,5 +1240,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.15.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=2.16.0 <3.0.0" + flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4130e3c27..c4bc842e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,11 +6,11 @@ repository: https://github.com/deckerst/aves # - github changelog: /CHANGELOG.md # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/1XXX.txt -version: 1.5.11+65 +version: 1.6.0+66 publish_to: none environment: - sdk: '>=2.15.0 <3.0.0' + sdk: '>=2.16.0 <3.0.0' # use `scripts/apply_flavor_{flavor}.sh` to set the right dependencies for the flavor dependencies: @@ -26,7 +26,7 @@ dependencies: collection: connectivity_plus: country_code: -# TODO TLAD as of 2021/12/01, null safe version is pre-release +# TODO TLAD as of 2022/01/30, null safe version is pre-release custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0' decorated_icon: device_info_plus: @@ -54,7 +54,7 @@ dependencies: overlay_support: package_info_plus: palette_generator: -# TODO TLAD as of 2021/12/01, latest version (v0.4.1) has this issue: https://github.com/zesage/panorama/issues/25 +# TODO TLAD as of 2022/01/30, latest version (v0.4.1) has this issue: https://github.com/zesage/panorama/issues/25 panorama: 0.4.0 pdf: percent_indicator: @@ -63,6 +63,8 @@ dependencies: provider: screen_brightness: shared_preferences: +# TODO TLAD as of 2022/02/12, latest version (v2.0.11) fails to load from analysis service (target wrong channel?) + shared_preferences_android: 2.0.10 sqflite: streams_channel: git: diff --git a/shaders_2.10.1.sksl.json b/shaders_2.10.1.sksl.json new file mode 100644 index 000000000..72c870270 --- /dev/null +++ b/shaders_2.10.1.sksl.json @@ -0,0 +1 @@ +{"platform":"android","name":"SM G970N","engineRevision":"ab46186b246f5a36bd1f3f295d14a43abb1e2f38","data":{"HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADnAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUKCXJldHVybiBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q29sb3JfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIAAIAAAABLCIABAAAAABAEGABBAMAACAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAdBAAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1MxX2MwX2MwLngsIHVjbGFtcF9TMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCS8vIEJsZW5kIG1vZGU6IE1vZHVsYXRlCglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPIEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdV8wX0luY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1MxX2MwWzRdOwp1bmlmb3JtIGhhbGY0IHVfMl9PZmZzZXRzX1MxX2MwWzRdOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIF9jb29yZHMpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF8zX2NvbG9yID0gaGFsZjQoMC4wKTsKCWZsb2F0MiBfNV9jb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgXzZfaSA9IDA7IChfNl9pIDwgMTMpOyBfNl9pKyspIChfM19jb2xvciArPSAoTWF0cml4RWZmZWN0X1MxX2MwX2MwKF9pbnB1dCwgKF81X2Nvb3JkICsgZmxvYXQyKCh1XzJfT2Zmc2V0c19TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSAqIHVfMF9JbmNyZW1lbnRfUzFfYzApKSkpICogdV8xX0tlcm5lbF9TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSkpOwoJcmV0dXJuIF8zX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAueSwgdWNsYW1wX1MxX2MwX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEzKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQFAAAAAAIAAEAAAABJKQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADAGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbMl07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbMl07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueCwgdWNsYW1wX1MxX2MwX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDYpOyBfNl9pKyspIChfM19jb2xvciArPSAoTWF0cml4RWZmZWN0X1MxX2MwX2MwKF9pbnB1dCwgKF81X2Nvb3JkICsgZmxvYXQyKCh1XzJfT2Zmc2V0c19TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSAqIHVfMF9JbmNyZW1lbnRfUzFfYzApKSkpICogdV8xX0tlcm5lbF9TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSkpOwoJcmV0dXJuIF8zX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgxKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQFAAAAAAABAEAAAABJKQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADAGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbMl07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbMl07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAueSwgdWNsYW1wX1MxX2MwX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDYpOyBfNl9pKyspIChfM19jb2xvciArPSAoTWF0cml4RWZmZWN0X1MxX2MwX2MwKF9pbnB1dCwgKF81X2Nvb3JkICsgZmxvYXQyKCh1XzJfT2Zmc2V0c19TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSAqIHVfMF9JbmNyZW1lbnRfUzFfYzApKSkpICogdV8xX0tlcm5lbF9TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSkpOwoJcmV0dXJuIF8zX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAAAAAAAXQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAA7AMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAeAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIBSQB5VRECGAEAAAMAAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAEAZCBRE4GNEACAAAOAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAABwUAAGNvbnN0IGludCBrRmlsbEFBX1MxID0gMTsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdWNpcmNsZV9TMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmNsZV9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgZDsKCWlmIChpbnQoMykgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDMpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQlkID0gaGFsZigobGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykgLSAxLjApICogdWNpcmNsZV9TMS56KTsKCX0KCWVsc2UgCgl7CgkJZCA9IGhhbGYoKDEuMCAtIGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpKSAqIHVjaXJjbGVfUzEueik7Cgl9CglpZiAoaW50KDMpID09IGtGaWxsQUFfUzEgfHwgaW50KDMpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQlyZXR1cm4gaGFsZjQoX2lucHV0ICogc2F0dXJhdGUoZCkpOwoJfQoJZWxzZSAKCXsKCQlyZXR1cm4gaGFsZjQoZCA+IDAuNSA/IF9pbnB1dCA6IGhhbGY0KDAuMCkpOwoJfQp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjbGVfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CAAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAuAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAARAGQWMHGBRIAAAAABQAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CAAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIsCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAEADZAAAAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAAPwQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5ID0gbWF4KHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHksIDAuMCk7CgloYWxmIHJpZ2h0QWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuUiAtIHNrX0ZyYWdDb29yZC54KSk7CgloYWxmIGJvdHRvbUFscGhhID0gaGFsZihzYXR1cmF0ZSh1aW5uZXJSZWN0X1MxLkIgLSBza19GcmFnQ29vcmQueSkpOwoJaGFsZiBhbHBoYSA9IGJvdHRvbUFscGhhICogcmlnaHRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAD1AwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAFAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2ULAAAAaW5DbGlwUGxhbmUADAAAAGluSXNlY3RQbGFuZQEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAAAQAAAAGQCBAMQACAAAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAIAAQAAAAAQGIA":"CAAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAMAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBDbGFtcGVkR3JhZGllbnRfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TMSkgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAABAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAIAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAD6AwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CgkvLyBCbGVuZCBtb2RlOiBNb2R1bGF0ZQoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAACEA2X4PLOGEAAAAAAAAACAAAAAVQQAAQAAAAAQCDIBCAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IAAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAEAZIA62YSBDACAAAGAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAAdwUAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueCwgdWNsYW1wX1MxX2MwX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEzKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQACAAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAADkAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5ID0gbWF4KHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHksIDAuMCk7CgloYWxmIHJpZ2h0QWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuUiAtIHNrX0ZyYWdDb29yZC54KSk7CgloYWxmIGJvdHRvbUFscGhhID0gaGFsZihzYXR1cmF0ZSh1aW5uZXJSZWN0X1MxLkIgLSBza19GcmFnQ29vcmQueSkpOwoJaGFsZiBhbHBoYSA9IGJvdHRvbUFscGhhICogcmlnaHRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAKAAAAaW5RdWFkRWRnZQAAAQAAAAAAAAA=","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CAAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","EABQAAAAAEAAAAAQAABQAAIOAAABCFYIAAKAUDAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CAAAAExTS1OhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8yX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA"}} \ No newline at end of file diff --git a/shaders_2.8.1.sksl.json b/shaders_2.8.1.sksl.json deleted file mode 100644 index d689bfb70..000000000 --- a/shaders_2.8.1.sksl.json +++ /dev/null @@ -1 +0,0 @@ -{"platform":"android","name":"SM G970N","engineRevision":"890a5fca2e34db413be624fc83aeea8e61d42ce6","data":{"HQQACAAAAAGAAAAAAIOP57ZDF37P7777777QCAAAAAAAAAAAMIAHSAAAAAAQAAAAAA4QAAAAAABAAAAACAZAAAAAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HQQAAAAAAAGAAAAAAIOP57ZDF37P7777777QCAAAAAAAAAAAMIAHSAAAAAAQAAAAAA4QAAAAAABAAAAACAZAAAAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAuAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","AWAAGAAAQAAIPCELAGEP777777777737AAAAAAAAAAAABZAAAAAAACAAAAAEBSAA":"CAAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","BUIBQAAAAQAAAAABBYIRP777777QAAAAAAAAAAAAZAAQAAAACAAAAAEASAAQAAAA":"CAAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IAAQAAAAAAAAA=","HQIAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAAAAAAAAFIAA2AAAAAAAAAQAAAACUAQACAAAAABQIMACCAYAAAAAAAAAAAADSAAAAAAAEAAAAAIDEAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADnAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUKCXJldHVybiBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q29sb3JfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HQIAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAAAAAAAAFIAA2AAAAAAQCAQAAAACUEQQCAAAAABQIMACCAYAAEAQAAAAAAADSAAAAAAAEAAAAAIDEAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAD6AwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CgkvLyBCbGVuZCBtb2RlOiBNb2R1bGF0ZQoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HQJAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAABAAAAAABBAMADCAB4QAAAAAEAAAAAAHEAAAAAAAIAAAAAQGIAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HQIAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAAAAAAAAVIBAYAAAAAAQAAIAAAACRRAAAAABAAAQAAAABIECAEAACAAAAAZQIEBSAAIAAAAAAAAJAAYAAAACAAAAAAACCAY":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueCwgdWNsYW1wX1MxX2MwX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzRfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF81X2kgPSAwOyAoXzVfaSA8IDEzKTsgXzVfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNF9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF81X2kgLyA0KV1bKF81X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF81X2kgLyA0KV1bKF81X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","C4QAAAAAMAAAAABAYAQ6FASCAEAAAABAAAAAAAAAAAAAAOIAAAAAAAQAAAABAMQA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HQIAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAAAAAAAAVIBAYAAAAAAACAIAAAACRRAAAAAAAEAQAAAABIECAAAQCAAAAAZQIEBSAAABAAAAAAAJAAYAAAACAAAAAAACCAY":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAueSwgdWNsYW1wX1MxX2MwX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzRfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF81X2kgPSAwOyAoXzVfaSA8IDEzKTsgXzVfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNF9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF81X2kgLyA0KV1bKF81X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF81X2kgLyA0KV1bKF81X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HRIACAAAAAMAAAAAAQ4AANCELQCDR7H7777QGAAAAAAAAAAAGQDCIUFDGAQAAAAAAIAAB7QAAAAP4AAAAD7AAAAA72MJ54XDUIAAAABQAAAAAYAAAAAEATA7TQ5QQAAAACYIR7H5AAAAAAABAAAAAMLCPLFI7CYCAAAMAAAAACAACAAAAAYX24HOEAAAAAGAELZPOAYAAAAAQAAAADCGATA7TQ5QQAAAAAAAAAAAFIBIXSG7B4AAAAAQAAAAAECDWCEPZ7IAAAAAAAAAAAADSAAAAAAAKAAAAAIDEAA":"CAAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAHAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgT3ZlcnJpZGVJbnB1dF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChDbGFtcGVkR3JhZGllbnRfUzFfYzAoaGFsZjQoMS4wLDEuMCwxLjAsMS4wKSkpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gT3ZlcnJpZGVJbnB1dF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfUzEpICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","BWABSAAAAQAAAAABB3777777AAKAAAAAAAAAAAAAZAAQAAAACAAAAAEASAAQAAAA":"CAAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAeAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","HQJAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAABAAAAAABBAMAEQAMAAAABAAAAAAABBAMAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HMMAAAAABBYIROAYQAAAAAAAAAAAAACABYAAAAEAAAAAAAEEBQAA":"CAAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAKAAAAaW5RdWFkRWRnZQAAAQAAAAAAAAA=","AWQAGAAAQAAIPCELAGEP777777777737AAAAAAAAAAAABZAAAAAAACAAAAAEBSAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HQIAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAAAAAAAAVIBACAABAAAAAMYECAZAAEAAAAAAAAEQAMAAAABAAAAAAABBAMAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","E5QQAAAAMAAGEADCACRAAAAAMAACFQDBABRAAIXCAIAAAABAA2IAOAAACAAAAAAASABQAAAAEAAAAAAAEEBQ":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAA7AMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HRIAAAAAAAMAAAAAAQ4PZ72HLQCDR7H7777QGAAAAAAAAAAAIQCQAAACAAAAAZQIAAAAAAAAAAAAAABAA4AAAACAAAAAAACCAYAAAAA":"CAAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIsCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","BWAAQAAAAQAAAAABB3777777AAKAAAAAAAAAAAAAZAAQAAAACAAAAAEASAAQAAAA":"CAAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","E5QQAAAAMAAGEADCACRAAAAAMAACFQDBABRAAIXCAIAAAABAGGAHWWEQIYAQAABQAAAAAAAAAAAEADQAAAAIAAAAAAAIIDAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAAdwUAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","AWQAGAAAQAAIPCELAGEP777777777737AAAAAAAAAAAIAGCADYAAAQAAAAAAAQAOAAAABAAAAAAABBAMAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HQJAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAABAAAAAABBAMAASANBMYOMDCQAAAAADAAAAAAAAAAAOIAAAAAAAQAAAABAMQAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AWQAGAAAQAAIPCELAGEP777777777737AAAAAAAAAAAIBRAA5ZQUCGQFAAAMAAAAAAAAAAAAAA4QAAAAAABAAAAACAZAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","C4SAAAAAMAAAAABAYDQ77H2CAEAAAABAAAAAAABAMQAAAOIAAAAAAAQAAAABAMQA":"CAAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","HQJAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAABAAAAAAJBAMADCAB4QAAAAAEAAAAAAHEAAAAAAAIAAAAAQGIAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","E5QQAAAAMAAGEADCACRAAAAAMAACFQDBABRAAIXCAIAAAAAAHEAAAAAAAIAAAAAQGIAAAAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAAAAAAAXQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","AWQQGAAAQAAIPCELAGEP777777777737AAAAAAAAAAAIBRAA5ZQUCGQFAAAMAAAAAAAAAAAAAA4QAAAAAABAAAAACAZAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HQQACAAAAAGAAAAAAIOP57ZDF37P7777777QCAAAAAAAAAAASABQAAAAEAAAAAAAEEBQAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","E5QQAAAAMAAGEADCACRAAAAAMAACFQDBABRAAIXCAIAAAABAGEQMJHBTJAAQAADQAAAAAAAAAAAEADQAAAAIAAAAAAAIIDAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAABwUAAGNvbnN0IGludCBrRmlsbEFBX1MxID0gMTsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdWNpcmNsZV9TMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmNsZV9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgZDsKCWlmIChpbnQoMykgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDMpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQlkID0gaGFsZigobGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykgLSAxLjApICogdWNpcmNsZV9TMS56KTsKCX0KCWVsc2UgCgl7CgkJZCA9IGhhbGYoKDEuMCAtIGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpKSAqIHVjaXJjbGVfUzEueik7Cgl9CglpZiAoaW50KDMpID09IGtGaWxsQUFfUzEgfHwgaW50KDMpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQlyZXR1cm4gaGFsZjQoX2lucHV0ICogc2F0dXJhdGUoZCkpOwoJfQoJZWxzZSAKCXsKCQlyZXR1cm4gaGFsZjQoZCA+IDAuNSA/IF9pbnB1dCA6IGhhbGY0KDAuMCkpOwoJfQp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjbGVfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","HRIAAAAAAAMAAAAAAQ4PZ72HLQCDR7H7777QGAAAAAAAAAAAGQDCIUFDGAQAAAAAAIAAB7QAAAAP4AAAAD7AAAAA72MJ54XDUIAAAABQAAAAAYAAAAAEATA7TQ5QQAAAACYIR7H5AAAAAAABAAAAAMLCPLFI7CYCAAAMAAAAACAACAAAAAYX24HOEAAAAAGAELZPOAYAAAAAQAAAADCGATA7TQ5QQAAAAAAAAAAAFIBIXSG7B4AAAAAQAAAAAECDWCEPZ7IAAAAAAAAAAAADSAAAAAAAEAAAAAIDEAA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAM8GAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgxKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBPdmVycmlkZUlucHV0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCgxLjAsMS4wLDEuMCwxLjApKSk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBPdmVycmlkZUlucHV0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HRJAAAAAAAMAAAAAAQ4PZ72HLQCDR7H7777QGAAAAACAAAAAACCAYABAA4AAAACAAAAAAACCAYAAA":"CAAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","GABQAAAAAEHBCFYCCYAAAAAAAEAAAACAIQAABSABAAAAAEAAAAAIBEABAA":"CAAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","HQQAAAAAAAGAAAAAAIOP57ZDF37P7777777QCAAAAAAAAAAACIBVPY6W4MIAAAAAAAAAEAAAABKBAABAAAAAAYEGQCEAAAAAAAAAAAAAOIAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HQIAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAAAAAAAAVIBACAIBAAAAAMYECAZAAEAQAAAAAAEQAMAAAABAAAAAAABBAMAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","C4QAAAAAMAAAAABAYAQ6FASCAEAAAABAAAAAAAAAAAACAMMAPNMJARQBAAADAAAAAAAAAAAAIAHAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","AWQAGAAAQAAIPCELAGEP777777777737AAAAAAAAAAAIAGGADYAAAQAAAAAAAQAOAAAABAAAAAAABBAMAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","C4QAAAAAMAAAAABAYAQ6FASCAEAAAABAAAAAAAAAAAACABUQA4AAAEAAAAAABEADAAAAAIAAAAAAAIIDAAAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HQIAAAAAAAGAAAAAAIOP577774BRZ7X7777QCAAAAAAAAAAAVIBAYAAAAAAAAAIAAAACRRAAAAAAAAAQAAAABIECAAAACAAAAAZQIEBSAAAAAAAAAAAJAAYAAAACAAAAAAACCAY":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPIEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdV8wX0luY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1MxX2MwWzRdOwp1bmlmb3JtIGhhbGY0IHVfMl9PZmZzZXRzX1MxX2MwWzRdOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIF9jb29yZHMpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF8zX2NvbG9yID0gaGFsZjQoMC4wKTsKCWZsb2F0MiBfNF9jb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgXzVfaSA9IDA7IChfNV9pIDwgMTMpOyBfNV9pKyspIChfM19jb2xvciArPSAoTWF0cml4RWZmZWN0X1MxX2MwX2MwKF9pbnB1dCwgKF80X2Nvb3JkICsgZmxvYXQyKCh1XzJfT2Zmc2V0c19TMV9jMFsoXzVfaSAvIDQpXVsoXzVfaSAmIDMpXSAqIHVfMF9JbmNyZW1lbnRfUzFfYzApKSkpICogdV8xX0tlcm5lbF9TMV9jMFsoXzVfaSAvIDQpXVsoXzVfaSAmIDMpXSkpOwoJcmV0dXJuIF8zX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HQQAAAAAAAGAAAAAAIOP57ZDF37P7777777QCAAAAAAAAAAASABQAAAAEAAAAAAAEEBQAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","AWQAGAAAQAAIPCELAGEP777777777737AAAAAAAAAAAIAGCAAIAAAQAAAAAAAQAOAAAABAAAAAAABBAMAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAADkAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5ID0gbWF4KHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHksIDAuMCk7CgloYWxmIHJpZ2h0QWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuUiAtIHNrX0ZyYWdDb29yZC54KSk7CgloYWxmIGJvdHRvbUFscGhhID0gaGFsZihzYXR1cmF0ZSh1aW5uZXJSZWN0X1MxLkIgLSBza19GcmFnQ29vcmQueSkpOwoJaGFsZiBhbHBoYSA9IGJvdHRvbUFscGhhICogcmlnaHRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AWQQGAAAQAAIPCELAGEP777777777737AAAAAAAAAAAABZAAAAAAACAAAAAEBSAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HRIAAAAAAAMAAAAAAQ4PZ72HLQCDR7H7777QGAAAAAAAAAAAGQDCIUFDGAQAAAAAAIAAB7QAAAAP4AAAAD7AAAAA72MJ54XDUIAAAAAQAAAAAYAAAAAEATA7TQ5QQAAAACYIR7H5AAAAAAABAAAAAMLCPLFI7CYCAAAEAAAAACAACAAAAAYX24HOEAAAAAGAELZPOAYAAAAAQAAAADCGATA7TQ5QQAAAAAAAAAAAFIBIXSG7B4AAAAAQAAAAAECDWCEPZ7IAAAAAAAAAAAADSAAAAAAAEAAAAAIDEAA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAM8GAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBPdmVycmlkZUlucHV0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCgxLjAsMS4wLDEuMCwxLjApKSk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBPdmVycmlkZUlucHV0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AWTQGAAAQAAIPCELAEEACCYBRP777737AAAAAAAAAAAABZAAAAAAACAAAAAEBSAA":"CAAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAD1AwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAFAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2ULAAAAaW5DbGlwUGxhbmUADAAAAGluSXNlY3RQbGFuZQEAAAAAAAAA"}} \ No newline at end of file diff --git a/test/fake/media_file_service.dart b/test/fake/media_file_service.dart index 70b5e07f4..22cbb16b5 100644 --- a/test/fake/media_file_service.dart +++ b/test/fake/media_file_service.dart @@ -11,7 +11,7 @@ class FakeMediaFileService extends Fake implements MediaFileService { Iterable entries, { required String newName, }) { - final contentId = FakeMediaStoreService.nextContentId; + final contentId = FakeMediaStoreService.nextId; final entry = entries.first; return Stream.value(MoveOpEvent( success: true, diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart index b32a368d9..c3ed0d756 100644 --- a/test/fake/media_store_service.dart +++ b/test/fake/media_store_service.dart @@ -9,27 +9,28 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { Set entries = {}; @override - Future> checkObsoleteContentIds(List knownContentIds) => SynchronousFuture([]); + Future> checkObsoleteContentIds(List knownContentIds) => SynchronousFuture([]); @override - Future> checkObsoletePaths(Map knownPathById) => SynchronousFuture([]); + Future> checkObsoletePaths(Map knownPathById) => SynchronousFuture([]); @override - Stream getEntries(Map knownEntries) => Stream.fromIterable(entries); + Stream getEntries(Map knownEntries, {String? directory}) => Stream.fromIterable(entries); - static var _lastContentId = 1; + static var _lastId = 1; - static int get nextContentId => _lastContentId++; + static int get nextId => _lastId++; static int get dateSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000; static AvesEntry newImage(String album, String filenameWithoutExtension) { - final contentId = nextContentId; + final id = nextId; final date = dateSecs; return AvesEntry( - uri: 'content://media/external/images/media/$contentId', - contentId: contentId, + id: id, + uri: 'content://media/external/images/media/$id', path: '$album/$filenameWithoutExtension.jpg', + contentId: id, pageId: null, sourceMimeType: MimeTypes.jpeg, width: 360, @@ -40,11 +41,12 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { dateModifiedSecs: date, sourceDateTakenMillis: date, durationMillis: null, + trashed: false, ); } static MoveOpEvent moveOpEventFor(AvesEntry entry, String sourceAlbum, String destinationAlbum) { - final newContentId = nextContentId; + final newContentId = nextId; return MoveOpEvent( success: true, skipped: false, diff --git a/test/fake/metadata_db.dart b/test/fake/metadata_db.dart index 40042d0f8..e532bd1aa 100644 --- a/test/fake/metadata_db.dart +++ b/test/fake/metadata_db.dart @@ -1,30 +1,36 @@ import 'package:aves/model/covers.dart'; +import 'package:aves/model/db/db_metadata.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; -import 'package:aves/model/metadata_db.dart'; +import 'package:aves/model/metadata/trash.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; class FakeMetadataDb extends Fake implements MetadataDb { + static int _lastId = 0; + + @override + int get nextId => ++_lastId; + @override Future init() => SynchronousFuture(null); @override - Future removeIds(Set contentIds, {Set? dataTypes}) => SynchronousFuture(null); + Future removeIds(Iterable ids, {Set? dataTypes}) => SynchronousFuture(null); // entries @override - Future> loadAllEntries() => SynchronousFuture({}); + Future> loadEntries({String? directory}) => SynchronousFuture({}); @override Future saveEntries(Iterable entries) => SynchronousFuture(null); @override - Future updateEntryId(int oldId, AvesEntry entry) => SynchronousFuture(null); + Future updateEntry(int id, AvesEntry entry) => SynchronousFuture(null); // date taken @@ -34,24 +40,35 @@ class FakeMetadataDb extends Fake implements MetadataDb { // catalog metadata @override - Future> loadAllMetadataEntries() => SynchronousFuture([]); + Future> loadCatalogMetadata() => SynchronousFuture({}); @override - Future saveMetadata(Set metadataEntries) => SynchronousFuture(null); + Future saveCatalogMetadata(Set metadataEntries) => SynchronousFuture(null); @override - Future updateMetadataId(int oldId, CatalogMetadata? metadata) => SynchronousFuture(null); + Future updateCatalogMetadata(int id, CatalogMetadata? metadata) => SynchronousFuture(null); // address @override - Future> loadAllAddresses() => SynchronousFuture([]); + Future> loadAddresses() => SynchronousFuture({}); @override Future saveAddresses(Set addresses) => SynchronousFuture(null); @override - Future updateAddressId(int oldId, AddressDetails? address) => SynchronousFuture(null); + Future updateAddress(int id, AddressDetails? address) => SynchronousFuture(null); + + // trash + + @override + Future clearTrashDetails() => SynchronousFuture(null); + + @override + Future> loadAllTrashDetails() => SynchronousFuture({}); + + @override + Future updateTrash(int id, TrashDetails? details) => SynchronousFuture(null); // favourites @@ -62,7 +79,7 @@ class FakeMetadataDb extends Fake implements MetadataDb { Future addFavourites(Iterable rows) => SynchronousFuture(null); @override - Future updateFavouriteId(int oldId, FavouriteRow row) => SynchronousFuture(null); + Future updateFavouriteId(int id, FavouriteRow row) => SynchronousFuture(null); @override Future removeFavourites(Iterable rows) => SynchronousFuture(null); @@ -76,7 +93,7 @@ class FakeMetadataDb extends Fake implements MetadataDb { Future addCovers(Iterable rows) => SynchronousFuture(null); @override - Future updateCoverEntryId(int oldId, CoverRow row) => SynchronousFuture(null); + Future updateCoverEntryId(int id, CoverRow row) => SynchronousFuture(null); @override Future removeCovers(Set filters) => SynchronousFuture(null); @@ -84,8 +101,5 @@ class FakeMetadataDb extends Fake implements MetadataDb { // video playback @override - Future updateVideoPlaybackId(int oldId, int? newId) => SynchronousFuture(null); - - @override - Future removeVideoPlayback(Set contentIds) => SynchronousFuture(null); + Future removeVideoPlayback(Iterable ids) => SynchronousFuture(null); } diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index b641e886a..68a3e9229 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -1,13 +1,14 @@ import 'dart:async'; +import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/availability.dart'; import 'package:aves/model/covers.dart'; +import 'package:aves/model/db/db_metadata.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; -import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/media_store_source.dart'; @@ -45,6 +46,7 @@ void main() { const aTag = 'sometag'; final australiaLatLng = LatLng(-26, 141); const australiaAddress = AddressDetails( + id: 0, countryCode: 'AU', countryName: 'AUS', ); @@ -81,7 +83,6 @@ void main() { } }); await source.init(); - await source.refresh(); await readyCompleter.future; return source; } @@ -96,7 +97,7 @@ void main() { (metadataFetchService as FakeMetadataFetchService).setUp( image1, CatalogMetadata( - contentId: image1.contentId, + id: image1.id, xmpSubjects: aTag, latitude: australiaLatLng.latitude, longitude: australiaLatLng.longitude, @@ -119,7 +120,7 @@ void main() { (metadataFetchService as FakeMetadataFetchService).setUp( image1, CatalogMetadata( - contentId: image1.contentId, + id: image1.id, xmpSubjects: aTag, latitude: australiaLatLng.latitude, longitude: australiaLatLng.longitude, @@ -129,7 +130,7 @@ void main() { final source = await _initSource(); expect(image1.tags, {aTag}); - expect(image1.addressDetails, australiaAddress.copyWith(contentId: image1.contentId)); + expect(image1.addressDetails, australiaAddress.copyWith(id: image1.id)); expect(source.visibleEntries.length, 0); expect(source.rawAlbums.length, 0); @@ -168,15 +169,15 @@ void main() { const albumFilter = AlbumFilter(testAlbum, 'whatever'); expect(albumFilter.test(image1), true); expect(covers.count, 0); - expect(covers.coverContentId(albumFilter), null); + expect(covers.coverEntryId(albumFilter), null); - await covers.set(albumFilter, image1.contentId); + await covers.set(albumFilter, image1.id); expect(covers.count, 1); - expect(covers.coverContentId(albumFilter), image1.contentId); + expect(covers.coverEntryId(albumFilter), image1.id); await covers.set(albumFilter, null); expect(covers.count, 0); - expect(covers.coverContentId(albumFilter), null); + expect(covers.coverEntryId(albumFilter), null); }); test('favourites and covers are kept when renaming entries', () async { @@ -188,13 +189,13 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); const albumFilter = AlbumFilter(testAlbum, 'whatever'); - await covers.set(albumFilter, image1.contentId); + await covers.set(albumFilter, image1.id); await source.renameEntry(image1, 'image1b.jpg', persist: true); expect(favourites.count, 1); expect(image1.isFavourite, true); expect(covers.count, 1); - expect(covers.coverContentId(albumFilter), image1.contentId); + expect(covers.coverEntryId(albumFilter), image1.id); }); test('favourites and covers are cleared when removing entries', () async { @@ -206,13 +207,13 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); final albumFilter = AlbumFilter(image1.directory!, 'whatever'); - await covers.set(albumFilter, image1.contentId); - await source.removeEntries({image1.uri}); + await covers.set(albumFilter, image1.id); + await source.removeEntries({image1.uri}, includeTrash: true); expect(source.rawAlbums.length, 0); expect(favourites.count, 0); expect(covers.count, 0); - expect(covers.coverContentId(albumFilter), null); + expect(covers.coverEntryId(albumFilter), null); }); test('albums are updated when moving entries', () async { @@ -232,8 +233,8 @@ void main() { await source.updateAfterMove( todoEntries: {image1}, - copy: false, - destinationAlbum: destinationAlbum, + moveType: MoveType.move, + destinationAlbums: {destinationAlbum}, movedOps: { FakeMediaStoreService.moveOpEventFor(image1, sourceAlbum, destinationAlbum), }, @@ -256,8 +257,8 @@ void main() { await source.updateAfterMove( todoEntries: {image1}, - copy: false, - destinationAlbum: destinationAlbum, + moveType: MoveType.move, + destinationAlbums: {destinationAlbum}, movedOps: { FakeMediaStoreService.moveOpEventFor(image1, sourceAlbum, destinationAlbum), }, @@ -277,12 +278,12 @@ void main() { final source = await _initSource(); expect(source.rawAlbums.length, 1); const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); - await covers.set(sourceAlbumFilter, image1.contentId); + await covers.set(sourceAlbumFilter, image1.id); await source.updateAfterMove( todoEntries: {image1}, - copy: false, - destinationAlbum: destinationAlbum, + moveType: MoveType.move, + destinationAlbums: {destinationAlbum}, movedOps: { FakeMediaStoreService.moveOpEventFor(image1, sourceAlbum, destinationAlbum), }, @@ -290,7 +291,7 @@ void main() { expect(source.rawAlbums.length, 2); expect(covers.count, 0); - expect(covers.coverContentId(sourceAlbumFilter), null); + expect(covers.coverEntryId(sourceAlbumFilter), null); }); test('favourites and covers are kept when renaming albums', () async { @@ -302,7 +303,7 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); var albumFilter = const AlbumFilter(sourceAlbum, 'whatever'); - await covers.set(albumFilter, image1.contentId); + await covers.set(albumFilter, image1.id); await source.renameAlbum(sourceAlbum, destinationAlbum, { image1 }, { @@ -313,7 +314,7 @@ void main() { expect(favourites.count, 1); expect(image1.isFavourite, true); expect(covers.count, 1); - expect(covers.coverContentId(albumFilter), image1.contentId); + expect(covers.coverEntryId(albumFilter), image1.id); }); testWidgets('unique album names', (tester) async { diff --git a/test/utils/geo_utils_test.dart b/test/utils/geo_utils_test.dart index 7c3f627ef..5f2cf7ecd 100644 --- a/test/utils/geo_utils_test.dart +++ b/test/utils/geo_utils_test.dart @@ -1,5 +1,5 @@ import 'package:aves/l10n/l10n.dart'; -import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/utils/geo_utils.dart'; import 'package:latlong2/latlong.dart'; import 'package:test/test.dart'; diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index 652746749..cc1a095be 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -1,6 +1,6 @@ import 'package:aves/main_play.dart' as app; import 'package:aves/model/settings/defaults.dart'; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; diff --git a/test_driver/driver_shaders.dart b/test_driver/driver_shaders.dart index e8fe57ad6..5c9a1e65e 100644 --- a/test_driver/driver_shaders.dart +++ b/test_driver/driver_shaders.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:aves/main_play.dart' as app; -import 'package:aves/model/settings/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test_driver/driver_shaders_test.dart b/test_driver/driver_shaders_test.dart index 710221e1c..69f34364e 100644 --- a/test_driver/driver_shaders_test.dart +++ b/test_driver/driver_shaders_test.dart @@ -33,8 +33,8 @@ void main() { unawaited(driver.close()); }); - test('scan media dir', () => driver.scanMediaDir(shadersTargetDirAndroid)); agreeToTerms(); + test('scan media dir', () => driver.scanMediaDir(shadersTargetDirAndroid)); visitAbout(); visitSettings(); sortCollection(); diff --git a/untranslated.json b/untranslated.json index 7a627567e..be3e30d53 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,12 +1,9 @@ { - "es": [ - "entryInfoActionEditLocation", - "exportEntryDialogWidth", - "exportEntryDialogHeight", - "editEntryLocationDialogTitle", - "editEntryLocationDialogChooseOnMapTooltip", - "editEntryLocationDialogLatitude", - "editEntryLocationDialogLongitude", - "locationPickerUseThisLocationButton" + "de": [ + "entryActionConvert" + ], + + "ru": [ + "entryActionConvert" ] } diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index b12b61c06..a6cb52012 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,6 +1,6 @@ Thanks for using Aves! -In v1.5.11: -- edit locations of images -- export SVGs to convert and resize them -- enjoy the app in Portuguese +In v1.6.0: +- recycle bin +- view small and large images at their actual size +- enjoy the app in Indonesian Full changelog available on GitHub \ No newline at end of file