diff --git a/.flutter b/.flutter index 2ad6cd72c..f72efea43 160000 --- a/.flutter +++ b/.flutter @@ -1 +1 @@ -Subproject commit 2ad6cd72c040113b47ee9055e722606a490ef0da +Subproject commit f72efea43c3013323d1b95cff571f3c1caa37583 diff --git a/.gitignore b/.gitignore index 60b39d58b..695e072ff 100644 --- a/.gitignore +++ b/.gitignore @@ -32,9 +32,6 @@ migrate_working_dir/ .pub/ /build/ -# Web related -lib/generated_plugin_registrant.dart - # Symbolication related app.*.symbols diff --git a/.metadata b/.metadata index fdef59c1a..0bc5b71a0 100644 --- a/.metadata +++ b/.metadata @@ -1,10 +1,30 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: bc7bc940836f1f834699625426795fd6f07c18ec - channel: beta + revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + - platform: android + create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a4b977c..e474e7a16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.8.5] - 2023-04-18 + +### Added + +- Collection: optional support for Samsung and Sony burst patterns +- Video: action to lock viewer +- Info: improved state/place display (requires rescan, limited to AU/GB/IN/US) +- Info: edit tags with state placeholder +- Info: show metadata from MP4 user data box +- Countries: show states for selected countries +- Tags: delete selected tags from all media in collection +- improved support for system font scale + +### Changed + +- upgraded Flutter to stable v3.7.11 +- when an album becomes empty, the folder will be deleted only if it is a non-app/common album +- TV: section header focus/highlight + +### Fixed + +- permission confusion when removable volume changes +- Viewer: flickering on first scale animation in some cases + ## [v1.8.4] - 2023-03-17 ### Added @@ -115,7 +139,8 @@ All notable changes to this project will be documented in this file. ### Changed -- editing description writes XMP `dc:description`, and clears Exif `ImageDescription` / `UserComment` +- editing description writes XMP `dc:description`, and clears Exif `ImageDescription` + / `UserComment` - in the tag editor, tapping on applied tag applies it to all items instead of removing it - pin app bar when selecting items diff --git a/android/.gitignore b/android/.gitignore index 0a741cb43..6f568019d 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -9,3 +9,5 @@ GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle index eb27e5c2e..140e804f3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -46,6 +46,16 @@ if (keystorePropertiesFile.exists()) { android { compileSdkVersion 33 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -148,6 +158,7 @@ android { // which lead to: UnsatisfiedLinkError...couldn't find "libflutter.so" // cf https://github.com/flutter/flutter/issues/37566#issuecomment-640879500 ndk { + //noinspection ChromeOsAbiSupport abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' } } @@ -183,9 +194,10 @@ repositories { dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' - implementation 'androidx.core:core-ktx:1.9.0' + implementation "androidx.appcompat:appcompat:1.6.1" + implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.exifinterface:exifinterface:1.3.6' - implementation 'androidx.lifecycle:lifecycle-process:2.5.1' + implementation 'androidx.lifecycle:lifecycle-process:2.6.1' implementation 'androidx.media:media:1.6.0' implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.security:security-crypto:1.1.0-alpha05' @@ -193,9 +205,9 @@ dependencies { implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.commonsware.cwac:document:0.5.0' implementation 'com.drewnoakes:metadata-extractor:2.18.0' - implementation 'com.github.bumptech.glide:glide:4.15.0' + implementation 'com.github.bumptech.glide:glide:4.15.1' // SLF4J implementation for `mp4parser` - implementation 'org.slf4j:slf4j-simple:2.0.6' + implementation 'org.slf4j:slf4j-simple:2.0.7' // forked, built by JitPack: // - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory @@ -210,7 +222,7 @@ dependencies { huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.8.0.300' kapt 'androidx.annotation:annotation:1.6.0' - kapt 'com.github.bumptech.glide:compiler:4.15.0' + kapt 'com.github.bumptech.glide:compiler:4.15.1' compileOnly rootProject.findProject(':streams_channel') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ee04303e8..5b85aa256 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ Gradle v7.4 / Android Gradle Plugin v7.3.0 recommend: - removing "package" from AndroidManifest.xml - adding it as "namespace" in app/build.gradle -This change eventually prevents building the app with Flutter v3.3.3. +This change eventually prevents building the app with Flutter v3.7.11. --> { when (val action = intent?.action) { Intent.ACTION_MAIN -> { + if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) { + return hashMapOf( + INTENT_DATA_KEY_SAFE_MODE to true, + ) + } intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page -> val filters = extractFiltersFromIntent(intent) return hashMapOf( @@ -393,7 +398,16 @@ open class MainActivity : FlutterFragmentActivity() { ) .build() - ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search)) + val safeMode = ShortcutInfoCompat.Builder(this, "safeMode") + .setShortLabel(getString(R.string.safe_mode_shortcut_short_label)) + .setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_safe_mode else R.drawable.ic_shortcut_safe_mode)) + .setIntent( + Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) + .putExtra(EXTRA_KEY_SAFE_MODE, true) + ) + .build() + + ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search, safeMode)) } private fun onAnalysisCompleted() { @@ -428,12 +442,14 @@ open class MainActivity : FlutterFragmentActivity() { const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" const val INTENT_DATA_KEY_PAGE = "page" const val INTENT_DATA_KEY_QUERY = "query" + const val INTENT_DATA_KEY_SAFE_MODE = "safeMode" const val INTENT_DATA_KEY_URI = "uri" const val INTENT_DATA_KEY_WIDGET_ID = "widgetId" const val EXTRA_KEY_PAGE = "page" const val EXTRA_KEY_FILTERS_ARRAY = "filters" const val EXTRA_KEY_FILTERS_STRING = "filtersString" + const val EXTRA_KEY_SAFE_MODE = "safeMode" const val EXTRA_KEY_WIDGET_ID = "widgetId" // request code to pending runnable 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 0ec5ace5c..812b86e91 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 @@ -38,10 +38,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.beyka.tiffbitmapfactory.TiffBitmapFactory import org.mp4parser.IsoFile -import org.mp4parser.PropertyBoxParserImpl -import org.mp4parser.boxes.iso14496.part12.FreeBox -import org.mp4parser.boxes.iso14496.part12.MediaDataBox -import org.mp4parser.boxes.iso14496.part12.SampleTableBox import java.io.FileInputStream import java.io.IOException @@ -341,23 +337,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { pfd.use { FileInputStream(it.fileDescriptor).use { stream -> stream.channel.use { channel -> - val boxParser = PropertyBoxParserImpl().apply { - val skippedTypes = listOf( - // parsing `MediaDataBox` can take a long time - MediaDataBox.TYPE, - // parsing `SampleTableBox` or `FreeBox` may yield OOM - SampleTableBox.TYPE, FreeBox.TYPE, - // some files are padded with `0` but the parser does not stop, reads type "0000", - // then a large size from following "0000", which may yield OOM - "0000", - ) - setBoxSkipper { type, size -> - if (skippedTypes.contains(type)) return@setBoxSkipper true - if (size > Mp4ParserHelper.BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large") - false - } - } - IsoFile(channel, boxParser).use { isoFile -> + IsoFile(channel, Mp4ParserHelper.metadataBoxParser()).use { isoFile -> isoFile.dumpBoxes(sb) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt index f35eaaa39..8a6a0b8d3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt @@ -42,6 +42,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { "canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context), "canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT), "canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M), + "canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O), "canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S), "canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N), "canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP), 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 1358fdb69..feda79f93 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 @@ -160,9 +160,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { thisDirName = "Spherical Video" metadataMap[thisDirName] = HashMap(GSpherical(bytes).describe()) } + QuickTimeMetadata.PROF_UUID -> { // redundant with info derived on the Dart side } + QuickTimeMetadata.USMT_UUID -> { val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA) val blocks = QuickTimeMetadata.parseUuidUsmt(bytes) @@ -187,6 +189,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } + else -> { val uuidPart = uuid.substringBefore('-') thisDirName = "${dir.name} $uuidPart" @@ -268,11 +271,13 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { // skip `Geo double/ascii params`, as their content is split and presented through various GeoTIFF keys ExifGeoTiffTags.TAG_GEO_DOUBLE_PARAMS, ExifGeoTiffTags.TAG_GEO_ASCII_PARAMS -> ArrayList() + else -> listOf(exifTagMapper(tag)) } }?.let { geoTiffDirMap.putAll(it) } byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) } } + mimeType == MimeTypes.DNG -> { // split DNG tags in their own directory val dngDirMap = metadataMap[DIR_DNG] ?: HashMap() @@ -281,9 +286,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { byDng[true]?.map { exifTagMapper(it) }?.let { dngDirMap.putAll(it) } byDng[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) } } + else -> dirMap.putAll(tags.map { exifTagMapper(it) }) } } + dir.isPngTextDir() -> { metadataMap.remove(thisDirName) dirMap = metadataMap[DIR_PNG_TEXTUAL_DATA] ?: HashMap() @@ -332,6 +339,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } + else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) }) } } @@ -406,6 +414,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } if (isVideo(mimeType)) { + // `metadata-extractor` do not extract custom tags in user data box + val userDataDir = Mp4ParserHelper.getUserData(context, mimeType, uri) + if (userDataDir.isNotEmpty()) { + metadataMap[Metadata.DIR_MP4_USER_DATA] = userDataDir + } + // this is used as fallback when the video metadata cannot be found on the Dart side // and to identify whether there is an accessible cover image // do not include HEIC here @@ -641,12 +655,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } + MimeTypes.GIF -> { // identification of animated GIF if (metadata.containsDirectoryOfType(GifAnimationDirectory::class.java)) { flags = flags or MASK_IS_ANIMATED } } + MimeTypes.WEBP -> { // identification of animated WEBP for (dir in metadata.getDirectoriesOfType(WebpDirectory::class.java)) { @@ -655,6 +671,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } + MimeTypes.TIFF -> { // identification of GeoTIFF for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { @@ -1119,16 +1136,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } + ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED -> { for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) { dir.getDateDigitizedMillis { dateMillis = it } } } + ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL -> { for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) { dir.getDateOriginalMillis { dateMillis = it } } } + GpsDirectory.TAG_DATE_STAMP -> { for (dir in metadata.getDirectoriesOfType(GpsDirectory::class.java)) { dir.gpsDate?.let { dateMillis = it.time } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt index c2e9d364d..c5f319dd2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt @@ -101,7 +101,17 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any endOfStream() } - private fun createFile() { + private suspend fun safeStartActivityForResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) { + if (intent.resolveActivity(activity.packageManager) != null) { + MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied) + activity.startActivityForResult(intent, requestCode) + } else { + MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}") + onDenied() + } + } + + private suspend fun createFile() { @SuppressLint("ObsoleteSdkInt") if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) @@ -116,12 +126,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any return } - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = mimeType - putExtra(Intent.EXTRA_TITLE, name) - } - MainActivity.pendingStorageAccessResultHandlers[MainActivity.CREATE_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri -> + fun onGranted(uri: Uri) { ioScope.launch { try { // truncate is necessary when overwriting a longer file @@ -134,13 +139,20 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any } endOfStream() } - }, { + } + + fun onDenied() { success(null) endOfStream() - }) - activity.startActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST) - } + } + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = mimeType + putExtra(Intent.EXTRA_TITLE, name) + } + safeStartActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied) + } private suspend fun openFile() { @SuppressLint("ObsoleteSdkInt") @@ -178,13 +190,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any addCategory(Intent.CATEGORY_OPENABLE) setTypeAndNormalize(mimeType ?: MimeTypes.ANY) } - if (intent.resolveActivity(activity.packageManager) != null) { - MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, ::onGranted, ::onDenied) - activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST) - } else { - MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}") - onDenied() - } + safeStartActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied) } private fun pickCollectionFilters() { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt index 54ac83280..699dc4009 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt @@ -33,6 +33,7 @@ object Metadata { const val DIR_DNG = "DNG" // custom const val DIR_EXIF_GEOTIFF = "GeoTIFF" // custom const val DIR_PNG_TEXTUAL_DATA = "PNG Textual Data" // custom + const val DIR_MP4_USER_DATA = "User Data" // custom // types of metadata const val TYPE_COMMENT = "comment" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt index 8edb548df..8bb9e096c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt @@ -2,11 +2,22 @@ package deckers.thibault.aves.metadata import android.content.Context import android.net.Uri +import android.util.Log +import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils +import deckers.thibault.aves.utils.toByteArray +import deckers.thibault.aves.utils.toHex import org.mp4parser.* +import org.mp4parser.boxes.UnknownBox import org.mp4parser.boxes.UserBox +import org.mp4parser.boxes.apple.AppleCoverBox import org.mp4parser.boxes.apple.AppleGPSCoordinatesBox +import org.mp4parser.boxes.apple.AppleItemListBox +import org.mp4parser.boxes.apple.AppleVariableSignedIntegerBox +import org.mp4parser.boxes.apple.Utf8AppleDataBox import org.mp4parser.boxes.iso14496.part12.* +import org.mp4parser.boxes.threegpp.ts26244.AuthorBox import org.mp4parser.support.AbstractBox import org.mp4parser.support.Matrix import org.mp4parser.tools.Path @@ -15,8 +26,10 @@ import java.io.FileInputStream import java.nio.channels.Channels object Mp4ParserHelper { + private val LOG_TAG = LogUtils.createTag() + // arbitrary size to detect boxes that may yield an OOM - const val BOX_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB + private const val BOX_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB fun computeEdits(context: Context, uri: Uri, modifier: (isoFile: IsoFile) -> Unit): List> { // we can skip uninteresting boxes with a seekable data source @@ -214,10 +227,8 @@ object Mp4ParserHelper { sb.appendLine("${"\t".repeat(indent)}[$boxType] ${box.javaClass.simpleName}") box.dumpBoxes(sb, indent + 1) } - is UserBox -> { - val userTypeHex = box.userType.joinToString("") { "%02x".format(it) } - sb.appendLine("${"\t".repeat(indent)}[$boxType] userType=$userTypeHex $box") - } + + is UserBox -> sb.appendLine("${"\t".repeat(indent)}[$boxType] userType=${box.userType.toHex()} $box") else -> sb.appendLine("${"\t".repeat(indent)}[$boxType] $box") } } catch (e: Exception) { @@ -227,10 +238,127 @@ object Mp4ParserHelper { } fun Box.toBytes(): ByteArray { + if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large") val stream = ByteArrayOutputStream(size.toInt()) Channels.newChannel(stream).use { getBox(it) } return stream.toByteArray() } + + fun metadataBoxParser() = PropertyBoxParserImpl().apply { + val skippedTypes = listOf( + // parsing `MediaDataBox` can take a long time + MediaDataBox.TYPE, + // parsing `SampleTableBox` or `FreeBox` may yield OOM + SampleTableBox.TYPE, FreeBox.TYPE, + // some files are padded with `0` but the parser does not stop, reads type "0000", + // then a large size from following "0000", which may yield OOM + "0000", + ) + setBoxSkipper { type, size -> + if (skippedTypes.contains(type)) return@setBoxSkipper true + if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large") + false + } + } + + fun getUserData( + context: Context, + mimeType: String, + uri: Uri, + ): MutableMap { + val fields = HashMap() + if (mimeType != MimeTypes.MP4) return fields + try { + // we can skip uninteresting boxes with a seekable data source + val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri") + pfd.use { + FileInputStream(it.fileDescriptor).use { stream -> + stream.channel.use { channel -> + // creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device` + IsoFile(channel, metadataBoxParser()).use { isoFile -> + val userDataBox = Path.getPath(isoFile.movieBox, UserDataBox.TYPE) + fields.putAll(extractBoxFields(userDataBox)) + } + } + } + } + } catch (e: NoClassDefFoundError) { + Log.w(LOG_TAG, "failed to parse MP4 for mimeType=$mimeType uri=$uri", e) + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get User Data box by MP4 parser for mimeType=$mimeType uri=$uri", e) + } + return fields + } + + private fun extractBoxFields(container: Container): HashMap { + val fields = HashMap() + for (box in container.boxes) { + if (box is AbstractBox && !box.isParsed) { + box.parseDetails() + } + val type = box.type + val key = boxTypeMetadataKey(type) + when (box) { + is AuthorBox -> fields[key] = box.author + is AppleCoverBox -> fields[key] = "[${box.coverData.size} bytes]" + is AppleGPSCoordinatesBox -> fields[key] = box.value + is AppleItemListBox -> fields.putAll(extractBoxFields(box)) + is AppleVariableSignedIntegerBox -> fields[key] = box.value.toString() + is Utf8AppleDataBox -> fields[key] = box.value + + is HandlerBox -> {} + is MetaBox -> { + val handlerBox = Path.getPath(box, HandlerBox.TYPE).apply { parseDetails() } + when (val handlerType = handlerBox?.handlerType ?: MetaBox.TYPE) { + "mdir" -> fields.putAll(extractBoxFields(box)) + else -> fields.putAll(extractBoxFields(box).map { Pair("$handlerType/${it.key}", it.value) }.toMap()) + } + } + + is UnknownBox -> { + val byteBuffer = box.data + val remaining = byteBuffer.remaining() + if (remaining > 512) { + fields[key] = "[$remaining bytes]" + } else { + val bytes = byteBuffer.toByteArray() + when (type) { + "SDLN", + "smrd" -> fields[key] = String(bytes) + + else -> fields[key] = "0x${bytes.toHex()}" + } + } + } + + else -> fields[key] = box.toString() + } + } + return fields + } + + // cf https://exiftool.org/TagNames/QuickTime.html + private fun boxTypeMetadataKey(type: String) = when (type) { + "auth" -> "Author" + "catg" -> "Category" + "covr" -> "Cover Art" + "keyw" -> "Keyword" + "mcvr" -> "Preview Image" + "pcst" -> "Podcast" + "SDLN" -> "Play Mode" + "stik" -> "Media Type" + "©alb" -> "Album" + "©ART" -> "Artist" + "©aut" -> "Author" + "©cmt" -> "Comment" + "©day" -> "Year" + "©des" -> "Description" + "©gen" -> "Genre" + "©nam" -> "Title" + "©too" -> "Encoder" + "©xyz" -> "GPS Coordinates" + else -> type + } } class Mp4TooLargeException(val type: String, message: String) : RuntimeException(message) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt index 6341ffe53..0c198ad29 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt @@ -1,5 +1,6 @@ package deckers.thibault.aves.metadata +import deckers.thibault.aves.utils.toHex import java.math.BigInteger import java.nio.charset.Charset import java.util.* @@ -51,7 +52,7 @@ object QuickTimeMetadata { // 0x01: string 0x01 -> String(payload, Charset.forName("UTF-16BE")).trim() // 0x101: artwork/icon - else -> "0x${payload.joinToString("") { "%02x".format(it) }}" + else -> "0x${payload.toHex()}" } val blockTypeString = when (blockType) { @@ -61,7 +62,7 @@ object QuickTimeMetadata { 0x0A -> "Track property" 0x0B -> "Time zone" 0x0C -> "Modification Time" - else -> "0x${"%02x".format(blockType)}" + else -> "0x${blockType.toByte().toHex()}" } blocks.add( diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index f618c0df1..cb721c9c1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -21,13 +21,9 @@ import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils import org.mp4parser.IsoFile -import org.mp4parser.PropertyBoxParserImpl import org.mp4parser.boxes.UserBox -import org.mp4parser.boxes.iso14496.part12.FreeBox -import org.mp4parser.boxes.iso14496.part12.MediaDataBox -import org.mp4parser.boxes.iso14496.part12.SampleTableBox import java.io.FileInputStream -import java.util.* +import java.util.TimeZone object XMP { private val LOG_TAG = LogUtils.createTag() @@ -156,26 +152,12 @@ object XMP { pfd.use { FileInputStream(it.fileDescriptor).use { stream -> stream.channel.use { channel -> - val boxParser = PropertyBoxParserImpl().apply { - val skippedTypes = listOf( - // parsing `MediaDataBox` can take a long time - MediaDataBox.TYPE, - // parsing `SampleTableBox` or `FreeBox` may yield OOM - SampleTableBox.TYPE, FreeBox.TYPE, - ) - setBoxSkipper { type, size -> - if (skippedTypes.contains(type)) return@setBoxSkipper true - if (size > Mp4ParserHelper.BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large") - false - } - } - // creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device` - // TODO TLAD [mp4] `IsoFile` init may fail if a skipped box has a `org.mp4parser.boxes.iso14496.part12.MetaBox` as parent, // because `MetaBox.parse()` changes the argument `dataSource` to a `RewindableReadableByteChannel`, // so it is no longer a seekable `FileChannel`, which is a requirement to skip boxes. - IsoFile(channel, boxParser).use { isoFile -> + // creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device` + IsoFile(channel, Mp4ParserHelper.metadataBoxParser()).use { isoFile -> isoFile.processBoxes(UserBox::class.java, true) { box, _ -> val boxSize = box.size if (MemoryUtils.canAllocate(boxSize)) { @@ -193,6 +175,8 @@ object XMP { } } } + } catch (e: NoClassDefFoundError) { + Log.w(LOG_TAG, "failed to parse MP4 for mimeType=$mimeType uri=$uri", e) } catch (e: Exception) { Log.w(LOG_TAG, "failed to get XMP by MP4 parser for mimeType=$mimeType uri=$uri", e) } 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 05fb4df29..c84596bea 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 @@ -815,6 +815,8 @@ abstract class ImageProvider { } } } + } catch (e: NoClassDefFoundError) { + callback.onFailure(e) } catch (e: Exception) { callback.onFailure(e) return false diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/ByteUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/ByteUtils.kt new file mode 100644 index 000000000..f45236ba4 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/ByteUtils.kt @@ -0,0 +1,13 @@ +package deckers.thibault.aves.utils + +import java.nio.ByteBuffer + +fun ByteBuffer.toByteArray(): ByteArray { + val bytes = ByteArray(remaining()) + get(bytes, 0, bytes.size) + return bytes +} + +fun ByteArray.toHex(): String = joinToString(separator = "") { it.toHex() } + +fun Byte.toHex(): String = "%02x".format(this) \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt index dc1377cf0..5275e0ed3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt @@ -17,7 +17,12 @@ import kotlin.coroutines.suspendCoroutine object FlutterUtils { private val LOG_TAG = LogUtils.createTag<FlutterUtils>() - suspend fun initFlutterEngine(context: Context, sharedPreferencesKey: String, callbackHandleKey: String, engineSetter: (engine: FlutterEngine) -> Unit) { + suspend fun initFlutterEngine( + context: Context, + sharedPreferencesKey: String, + callbackHandleKey: String, + engineSetter: (engine: FlutterEngine) -> Unit, + ) { val callbackHandle = context.getSharedPreferences(sharedPreferencesKey, Context.MODE_PRIVATE).getLong(callbackHandleKey, 0) if (callbackHandle == 0L) { Log.e(LOG_TAG, "failed to retrieve registered callback handle for sharedPreferencesKey=$sharedPreferencesKey callbackHandleKey=$callbackHandleKey") 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 0996f0a5d..6863489fd 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 @@ -195,11 +195,8 @@ object PermissionManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // cf https://developer.android.com/about/versions/11/privacy/storage#directory-access dirs.add(Environment.DIRECTORY_DOWNLOADS) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // by observation, no documentation - dirs.add("Android") - } + // depends on device, no documentation + dirs.add("Android") } return dirs } 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 c73694c99..3de3ca4c0 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,11 +33,23 @@ import java.util.regex.Pattern object StorageUtils { private val LOG_TAG = LogUtils.createTag<StorageUtils>() - // from `DocumentsContract` + private const val SCHEME_CONTENT = ContentResolver.SCHEME_CONTENT + + // cf DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents" + + // cf DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID private const val EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID = "primary" - private const val TREE_URI_ROOT = "content://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/" + private const val TREE_URI_ROOT = "$SCHEME_CONTENT://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/" + + private val MEDIA_STORE_VOLUME_EXTERNAL = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaStore.VOLUME_EXTERNAL else "external" + + // TODO TLAD get it from `MediaStore.Images.Media.EXTERNAL_CONTENT_URI`? + private val IMAGE_PATH_ROOT = "/$MEDIA_STORE_VOLUME_EXTERNAL/images/" + + // TODO TLAD get it from `MediaStore.Video.Media.EXTERNAL_CONTENT_URI`? + private val VIDEO_PATH_ROOT = "/$MEDIA_STORE_VOLUME_EXTERNAL/video/" private val UUID_PATTERN = Regex("[A-Fa-f\\d-]+") private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)") @@ -348,7 +360,17 @@ object StorageUtils { // fallback when UUID does not appear in the SD card volume path val primaryVolumePath = getPrimaryVolumePath(context) - getVolumePaths(context).firstOrNull { it != primaryVolumePath }?.let { return it } + getVolumePaths(context).firstOrNull { volumePath -> + if (volumePath == primaryVolumePath) { + false + } else { + // exclude volumes that use regular naming scheme with UUID in them + // to prevent returning path with the UUID of a new volume + // when the argument is the UUID of an obsolete volume + val volumeUuid = volumePath.split(File.separator).lastOrNull { it.isNotEmpty() } + !(volumeUuid == null || volumeUuid.matches(UUID_PATTERN)) + } + }?.let { return it } Log.e(LOG_TAG, "failed to find volume path for UUID=$uuid") return null @@ -535,7 +557,7 @@ object StorageUtils { uri ?: return false // a URI's authority is [userinfo@]host[:port] // but we only want the host when comparing to Media Store's "authority" - return ContentResolver.SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true) + return SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true) } fun getOriginalUri(context: Context, uri: Uri): Uri { @@ -544,7 +566,7 @@ object StorageUtils { val path = uri.path path ?: return uri // from Android 11, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException` - if (path.startsWith("/external/images/") || path.startsWith("/external/video/")) { + if (path.startsWith(IMAGE_PATH_ROOT) || path.startsWith(VIDEO_PATH_ROOT)) { // "Caller must hold ACCESS_MEDIA_LOCATION permission to access original" if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) { return MediaStore.setRequireOriginal(uri) @@ -601,7 +623,7 @@ object StorageUtils { return uri } - // Build a typical `images` or `videos` content URI from the original content ID. + // Build a typical `images` or `video` 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? { diff --git a/android/app/src/main/res/drawable-v21/ic_shortcut_safe_mode.xml b/android/app/src/main/res/drawable-v21/ic_shortcut_safe_mode.xml new file mode 100644 index 000000000..dc918373c --- /dev/null +++ b/android/app/src/main/res/drawable-v21/ic_shortcut_safe_mode.xml @@ -0,0 +1,16 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> + <path + android:fillColor="@color/ic_shortcut_background" + android:pathData="M0,24 A1,1 0 1,1 48,24 A1,1 0 1,1 0,24" /> + <group + android:translateX="12" + android:translateY="12"> + <path + android:fillColor="@color/ic_shortcut_foreground" + android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM19.46,9.12l-2.78,1.15c-0.51,-1.36 -1.58,-2.44 -2.95,-2.94l1.15,-2.78C16.98,5.35 18.65,7.02 19.46,9.12zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3S13.66,15 12,15zM9.13,4.54l1.17,2.78c-1.38,0.5 -2.47,1.59 -2.98,2.97L4.54,9.13C5.35,7.02 7.02,5.35 9.13,4.54zM4.54,14.87l2.78,-1.15c0.51,1.38 1.59,2.46 2.97,2.96l-1.17,2.78C7.02,18.65 5.35,16.98 4.54,14.87zM14.88,19.46l-1.15,-2.78c1.37,-0.51 2.45,-1.59 2.95,-2.97l2.78,1.17C18.65,16.98 16.98,18.65 14.88,19.46z" /> + </group> +</vector> diff --git a/android/app/src/main/res/drawable-v26/ic_shortcut_safe_mode_foreground.xml b/android/app/src/main/res/drawable-v26/ic_shortcut_safe_mode_foreground.xml new file mode 100644 index 000000000..fe957d665 --- /dev/null +++ b/android/app/src/main/res/drawable-v26/ic_shortcut_safe_mode_foreground.xml @@ -0,0 +1,16 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:tint="@color/ic_shortcut_foreground" + android:viewportWidth="108" + android:viewportHeight="108"> + <group + android:scaleX="1.7226" + android:scaleY="1.7226" + android:translateX="33.3288" + android:translateY="33.3288"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM19.46,9.12l-2.78,1.15c-0.51,-1.36 -1.58,-2.44 -2.95,-2.94l1.15,-2.78C16.98,5.35 18.65,7.02 19.46,9.12zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3S13.66,15 12,15zM9.13,4.54l1.17,2.78c-1.38,0.5 -2.47,1.59 -2.98,2.97L4.54,9.13C5.35,7.02 7.02,5.35 9.13,4.54zM4.54,14.87l2.78,-1.15c0.51,1.38 1.59,2.46 2.97,2.96l-1.17,2.78C7.02,18.65 5.35,16.98 4.54,14.87zM14.88,19.46l-1.15,-2.78c1.37,-0.51 2.45,-1.59 2.95,-2.97l2.78,1.17C18.65,16.98 16.98,18.65 14.88,19.46z" /> + </group> +</vector> diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_safe_mode.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_safe_mode.xml new file mode 100644 index 000000000..40f9ff006 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_safe_mode.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@color/ic_shortcut_background" /> + <foreground android:drawable="@drawable/ic_shortcut_safe_mode_foreground" /> +</adaptive-icon> \ No newline at end of file diff --git a/android/app/src/main/res/values-cs/strings.xml b/android/app/src/main/res/values-cs/strings.xml index 171ff70fa..02638c56a 100644 --- a/android/app/src/main/res/values-cs/strings.xml +++ b/android/app/src/main/res/values-cs/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_notification_default_title">Prohledávání médií</string> <string name="analysis_notification_action_stop">Zastavit</string> <string name="app_widget_label">Fotorámeček</string> + <string name="safe_mode_shortcut_short_label">Bezpečný režim</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index 1fa49e8a6..1ecb251c5 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_service_description">Bilder &amp; Videos scannen</string> <string name="analysis_notification_default_title">Medien scannen</string> <string name="analysis_notification_action_stop">Abbrechen</string> + <string name="safe_mode_shortcut_short_label">Sicherer Modus</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml index 3c656e5b6..b2364e4dd 100644 --- a/android/app/src/main/res/values-es/strings.xml +++ b/android/app/src/main/res/values-es/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_service_description">Explorar imágenes &amp; videos</string> <string name="analysis_notification_default_title">Explorando medios</string> <string name="analysis_notification_action_stop">Anular</string> + <string name="safe_mode_shortcut_short_label">Modo seguro</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-eu/strings.xml b/android/app/src/main/res/values-eu/strings.xml index ca50b6ea6..92a69bac7 100644 --- a/android/app/src/main/res/values-eu/strings.xml +++ b/android/app/src/main/res/values-eu/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_notification_action_stop">Gelditu</string> <string name="analysis_notification_default_title">Media eskaneatzen</string> <string name="app_name">Aves</string> + <string name="safe_mode_shortcut_short_label">Modu segurua</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml index 6104e3059..0243698d8 100644 --- a/android/app/src/main/res/values-fr/strings.xml +++ b/android/app/src/main/res/values-fr/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_service_description">Analyse des images &amp; vidéos</string> <string name="analysis_notification_default_title">Analyse des images</string> <string name="analysis_notification_action_stop">Annuler</string> + <string name="safe_mode_shortcut_short_label">Mode sans échec</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-hi/strings.xml b/android/app/src/main/res/values-hi/strings.xml new file mode 100644 index 000000000..615529ee1 --- /dev/null +++ b/android/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="analysis_notification_default_title">मीडिया जाँचा जा राहा है</string> + <string name="analysis_notification_action_stop">रोके</string> + <string name="app_widget_label">फोटो फ्रेम</string> + <string name="wallpaper">वॉलपेपर</string> + <string name="search_shortcut_short_label">खोजें</string> + <string name="analysis_channel_name">मीडिया जाँचे</string> + <string name="app_name">ऐवीज</string> + <string name="videos_shortcut_short_label">वीडियो</string> + <string name="analysis_service_description">छवि &amp; वीडियो जाँचे</string> +</resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-hu/strings.xml b/android/app/src/main/res/values-hu/strings.xml new file mode 100644 index 000000000..20b83b718 --- /dev/null +++ b/android/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">Aves</string> + <string name="wallpaper">Háttérkép</string> + <string name="search_shortcut_short_label">Keresés</string> + <string name="videos_shortcut_short_label">Videók</string> + <string name="analysis_notification_action_stop">Állj</string> +</resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-id/strings.xml b/android/app/src/main/res/values-id/strings.xml index 4a074015a..c8067fc0e 100644 --- a/android/app/src/main/res/values-id/strings.xml +++ b/android/app/src/main/res/values-id/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_service_description">Pindai gambar &amp; video</string> <string name="analysis_notification_default_title">Memindai media</string> <string name="analysis_notification_action_stop">Berhenti</string> + <string name="safe_mode_shortcut_short_label">Mode aman</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-it/strings.xml b/android/app/src/main/res/values-it/strings.xml index cffcd1296..1f254b126 100644 --- a/android/app/src/main/res/values-it/strings.xml +++ b/android/app/src/main/res/values-it/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_service_description">Scansione immagini &amp; videos</string> <string name="analysis_notification_default_title">Scansione in corso</string> <string name="analysis_notification_action_stop">Annulla</string> + <string name="safe_mode_shortcut_short_label">Modalità provvisoria</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml index d7f678e4b..7204d99c5 100644 --- a/android/app/src/main/res/values-ja/strings.xml +++ b/android/app/src/main/res/values-ja/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_service_description">画像と動画をスキャン</string> <string name="analysis_notification_default_title">メディアをスキャン中</string> <string name="analysis_notification_action_stop">停止</string> + <string name="safe_mode_shortcut_short_label">セーフモード</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-ko/strings.xml b/android/app/src/main/res/values-ko/strings.xml index a226d003b..0c064c633 100644 --- a/android/app/src/main/res/values-ko/strings.xml +++ b/android/app/src/main/res/values-ko/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_service_description">사진과 동영상 분석</string> <string name="analysis_notification_default_title">미디어 분석</string> <string name="analysis_notification_action_stop">취소</string> + <string name="safe_mode_shortcut_short_label">안전 모드</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-nb-rNO/strings.xml b/android/app/src/main/res/values-nb-rNO/strings.xml index fb6d8bcfb..d1759646b 100644 --- a/android/app/src/main/res/values-nb-rNO/strings.xml +++ b/android/app/src/main/res/values-nb-rNO/strings.xml @@ -9,4 +9,5 @@ <string name="wallpaper">Bakgrunnsbilde</string> <string name="search_shortcut_short_label">Søk</string> <string name="analysis_notification_action_stop">Stopp</string> + <string name="safe_mode_shortcut_short_label">Trygt modus</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml index 28f72abad..ed2f0b129 100644 --- a/android/app/src/main/res/values-pl/strings.xml +++ b/android/app/src/main/res/values-pl/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_notification_action_stop">Zatrzymaj</string> <string name="app_name">Aves</string> <string name="wallpaper">Tapeta</string> + <string name="safe_mode_shortcut_short_label">Tryb bezpieczny</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-ro/strings.xml b/android/app/src/main/res/values-ro/strings.xml index 75837c05f..bb3475bf8 100644 --- a/android/app/src/main/res/values-ro/strings.xml +++ b/android/app/src/main/res/values-ro/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_notification_default_title">Scanarea suporturilor</string> <string name="analysis_notification_action_stop">Stop</string> <string name="search_shortcut_short_label">Căutare</string> + <string name="safe_mode_shortcut_short_label">Modul de siguranță</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 65f801bc9..84259422b 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_service_description">Сканировать изображения и видео</string> <string name="analysis_notification_default_title">Сканирование медиа</string> <string name="analysis_notification_action_stop">Стоп</string> + <string name="safe_mode_shortcut_short_label">Безопасный режим</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values-uk/strings.xml b/android/app/src/main/res/values-uk/strings.xml index ddcd946ed..b6c0c0a68 100644 --- a/android/app/src/main/res/values-uk/strings.xml +++ b/android/app/src/main/res/values-uk/strings.xml @@ -9,4 +9,5 @@ <string name="analysis_notification_action_stop">Стоп</string> <string name="app_widget_label">Фоторамка</string> <string name="analysis_notification_default_title">Сканування медіа</string> + <string name="safe_mode_shortcut_short_label">Безпечний режим</string> </resources> \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index cb2b215c0..f0f317c96 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ <string name="app_name">Aves</string> <string name="app_widget_label">Photo Frame</string> <string name="wallpaper">Wallpaper</string> + <string name="safe_mode_shortcut_short_label">Safe mode</string> <string name="search_shortcut_short_label">Search</string> <string name="videos_shortcut_short_label">Videos</string> <string name="analysis_channel_name">Media scan</string> diff --git a/android/build.gradle b/android/build.gradle index f15f63e03..c9ccd0367 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,7 +1,6 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext { - kotlin_version = '1.7.20' + kotlin_version = '1.8.0' abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4] useCrashlytics = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("play") } useHms = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("huawei") } @@ -18,8 +17,7 @@ buildscript { } dependencies { - // TODO TLAD upgrade Android Gradle plugin >=7.3 when this is fixed: https://github.com/flutter/flutter/issues/115100 - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" if (useCrashlytics) { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3a528264c..3c472b99c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,3 @@ -#Thu Oct 22 10:54:33 KST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/fastlane/metadata/android/de/short_description.txt b/fastlane/metadata/android/de/short_description.txt index 9f8c85a29..66ef72309 100644 --- a/fastlane/metadata/android/de/short_description.txt +++ b/fastlane/metadata/android/de/short_description.txt @@ -1 +1 @@ -Galerie und Metadata Explorer \ No newline at end of file +Galerie und Metadaten Explorer \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/96.txt b/fastlane/metadata/android/en-US/changelogs/96.txt new file mode 100644 index 000000000..596f60dac --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/96.txt @@ -0,0 +1,5 @@ +In v1.8.5: +- navigate states for some countries (requires rescan) +- group Samsung and Sony bursts +- lock viewer when watching videos +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/9601.txt b/fastlane/metadata/android/en-US/changelogs/9601.txt new file mode 100644 index 000000000..596f60dac --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/9601.txt @@ -0,0 +1,5 @@ +In v1.8.5: +- navigate states for some countries (requires rescan) +- group Samsung and Sony bursts +- lock viewer when watching videos +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/hi/full_description.txt b/fastlane/metadata/android/hi/full_description.txt new file mode 100644 index 000000000..6c92748f8 --- /dev/null +++ b/fastlane/metadata/android/hi/full_description.txt @@ -0,0 +1,5 @@ +<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files. + +<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc. + +<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>. \ No newline at end of file diff --git a/fastlane/metadata/android/hi/short_description.txt b/fastlane/metadata/android/hi/short_description.txt new file mode 100644 index 000000000..ba793e562 --- /dev/null +++ b/fastlane/metadata/android/hi/short_description.txt @@ -0,0 +1 @@ +गैलरी और मोटाडेटा एक्स्प्लोरर \ No newline at end of file diff --git a/fastlane/metadata/android/hu/full_description.txt b/fastlane/metadata/android/hu/full_description.txt new file mode 100644 index 000000000..6c92748f8 --- /dev/null +++ b/fastlane/metadata/android/hu/full_description.txt @@ -0,0 +1,5 @@ +<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files. + +<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc. + +<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>. \ No newline at end of file diff --git a/fastlane/metadata/android/hu/short_description.txt b/fastlane/metadata/android/hu/short_description.txt new file mode 100644 index 000000000..8c9445bd5 --- /dev/null +++ b/fastlane/metadata/android/hu/short_description.txt @@ -0,0 +1 @@ +Gallery and metadata explorer \ No newline at end of file diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt index a095036ca..9cae013fe 100644 --- a/fastlane/metadata/android/pt-BR/full_description.txt +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -2,4 +2,4 @@ <b>Navegação e pesquisa</b> é uma parte importante do <i>Aves</i>. O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc. -<i>Aves</i> integra com Android (de <b>API 19 para 33</b>, i.e. de KitKat para Android 13) com recursos como <b>atalhos de apps</b> e <b>pesquisa global</b> manipulação. Também funciona como um <b>visualizador e selecionador de mídia</b>. \ No newline at end of file +<i>Aves</i> integra com Android (de KitKat até Android 13, incluindo TVs Android) com recursos como <b>widgets</b>, <b>atalhos de apps</b>, <b>protetor de tela</b> e <b>pesquisa global</b>. Também funciona como um <b>visualizador e selecionador de mídia</b>. \ No newline at end of file diff --git a/lib/convert/convert.dart b/lib/convert/convert.dart new file mode 100644 index 000000000..43780fce2 --- /dev/null +++ b/lib/convert/convert.dart @@ -0,0 +1,3 @@ +export 'metadata/date_field_source.dart'; +export 'metadata/fields.dart'; +export 'metadata/metadata_type.dart'; diff --git a/lib/convert/metadata/date_field_source.dart b/lib/convert/metadata/date_field_source.dart new file mode 100644 index 000000000..1bc1ee1bd --- /dev/null +++ b/lib/convert/metadata/date_field_source.dart @@ -0,0 +1,18 @@ +import 'package:aves_model/aves_model.dart'; + +extension ExtraDateFieldSourceConvert on DateFieldSource { + MetadataField? toMetadataField() { + switch (this) { + case DateFieldSource.fileModifiedDate: + return null; + case DateFieldSource.exifDate: + return MetadataField.exifDate; + case DateFieldSource.exifDateOriginal: + return MetadataField.exifDateOriginal; + case DateFieldSource.exifDateDigitized: + return MetadataField.exifDateDigitized; + case DateFieldSource.exifGpsDate: + return MetadataField.exifGpsDatestamp; + } + } +} diff --git a/lib/model/metadata/fields.dart b/lib/convert/metadata/fields.dart similarity index 65% rename from lib/model/metadata/fields.dart rename to lib/convert/metadata/fields.dart index 17a344aa6..fd677c105 100644 --- a/lib/model/metadata/fields.dart +++ b/lib/convert/metadata/fields.dart @@ -1,87 +1,6 @@ -import 'package:aves/model/metadata/enums/enums.dart'; +import 'package:aves_model/aves_model.dart'; -enum MetadataField { - exifDate, - exifDateOriginal, - exifDateDigitized, - exifGpsAltitude, - exifGpsAltitudeRef, - exifGpsAreaInformation, - exifGpsDatestamp, - exifGpsDestBearing, - exifGpsDestBearingRef, - exifGpsDestDistance, - exifGpsDestDistanceRef, - exifGpsDestLatitude, - exifGpsDestLatitudeRef, - exifGpsDestLongitude, - exifGpsDestLongitudeRef, - exifGpsDifferential, - exifGpsDOP, - exifGpsHPositioningError, - exifGpsImgDirection, - exifGpsImgDirectionRef, - exifGpsLatitude, - exifGpsLatitudeRef, - exifGpsLongitude, - exifGpsLongitudeRef, - exifGpsMapDatum, - exifGpsMeasureMode, - exifGpsProcessingMethod, - exifGpsSatellites, - exifGpsSpeed, - exifGpsSpeedRef, - exifGpsStatus, - exifGpsTimestamp, - exifGpsTrack, - exifGpsTrackRef, - exifGpsVersionId, - exifImageDescription, - exifUserComment, - mp4GpsCoordinates, - mp4RotationDegrees, - mp4Xmp, - xmpXmpCreateDate, -} - -class MetadataFields { - static const Set<MetadataField> exifGpsFields = { - MetadataField.exifGpsAltitude, - MetadataField.exifGpsAltitudeRef, - MetadataField.exifGpsAreaInformation, - MetadataField.exifGpsDatestamp, - MetadataField.exifGpsDestBearing, - MetadataField.exifGpsDestBearingRef, - MetadataField.exifGpsDestDistance, - MetadataField.exifGpsDestDistanceRef, - MetadataField.exifGpsDestLatitude, - MetadataField.exifGpsDestLatitudeRef, - MetadataField.exifGpsDestLongitude, - MetadataField.exifGpsDestLongitudeRef, - MetadataField.exifGpsDifferential, - MetadataField.exifGpsDOP, - MetadataField.exifGpsHPositioningError, - MetadataField.exifGpsImgDirection, - MetadataField.exifGpsImgDirectionRef, - MetadataField.exifGpsLatitude, - MetadataField.exifGpsLatitudeRef, - MetadataField.exifGpsLongitude, - MetadataField.exifGpsLongitudeRef, - MetadataField.exifGpsMapDatum, - MetadataField.exifGpsMeasureMode, - MetadataField.exifGpsProcessingMethod, - MetadataField.exifGpsSatellites, - MetadataField.exifGpsSpeed, - MetadataField.exifGpsSpeedRef, - MetadataField.exifGpsStatus, - MetadataField.exifGpsTimestamp, - MetadataField.exifGpsTrack, - MetadataField.exifGpsTrackRef, - MetadataField.exifGpsVersionId, - }; -} - -extension ExtraMetadataField on MetadataField { +extension ExtraMetadataFieldConvert on MetadataField { MetadataType get type { switch (this) { case MetadataField.exifDate: @@ -228,21 +147,4 @@ extension ExtraMetadataField on MetadataField { return null; } } - - String get title { - switch (this) { - case MetadataField.exifDate: - return 'Exif date'; - case MetadataField.exifDateOriginal: - return 'Exif original date'; - case MetadataField.exifDateDigitized: - return 'Exif digitized date'; - case MetadataField.exifGpsDatestamp: - return 'Exif GPS date'; - case MetadataField.xmpXmpCreateDate: - return 'XMP xmp:CreateDate'; - default: - return name; - } - } } diff --git a/lib/convert/metadata/metadata_type.dart b/lib/convert/metadata/metadata_type.dart new file mode 100644 index 000000000..63259e7ba --- /dev/null +++ b/lib/convert/metadata/metadata_type.dart @@ -0,0 +1,28 @@ +import 'package:aves_model/aves_model.dart'; + +extension ExtraMetadataTypeConvert on MetadataType { + String get toPlatform { + switch (this) { + case MetadataType.comment: + return 'comment'; + case MetadataType.exif: + return 'exif'; + case MetadataType.iccProfile: + return 'icc_profile'; + case MetadataType.iptc: + return 'iptc'; + case MetadataType.jfif: + return 'jfif'; + case MetadataType.jpegAdobe: + return 'jpeg_adobe'; + case MetadataType.jpegDucky: + return 'jpeg_ducky'; + case MetadataType.mp4: + return 'mp4'; + case MetadataType.photoshopIrb: + return 'photoshop_irb'; + case MetadataType.xmp: + return 'xmp'; + } + } +} diff --git a/lib/geo/states.dart b/lib/geo/states.dart new file mode 100644 index 000000000..c7b01b3e8 --- /dev/null +++ b/lib/geo/states.dart @@ -0,0 +1,140 @@ +import 'package:aves/ref/unicode.dart'; +import 'package:country_code/country_code.dart'; + +class GeoStates { + static final aus = CountryCode.AU.alpha2; + static final gbr = CountryCode.GB.alpha2; + static final ind = CountryCode.IN.alpha2; + static final usa = CountryCode.US.alpha2; + + static final Set<String> stateCountryCodes = { + aus, + gbr, + ind, + usa, + }; + + static final stateCodesByCountryCode = { + aus: EmojiStateCodes.aus, + gbr: EmojiStateCodes.gbr, + ind: EmojiStateCodes.ind, + usa: EmojiStateCodes.usa, + }; + + static const stateCodeByName = { + ..._australiaEnglish, + ..._indiaEnglish, + ..._unitedKingdomEnglish, + ..._unitedStatesEnglish, + }; + + static const _australiaEnglish = { + 'Australian Capital Territory': EmojiStateCodes.auAustralianCapitalTerritory, + 'New South Wales': EmojiStateCodes.auNewSouthWales, + 'Northern Territory': EmojiStateCodes.auNorthernTerritory, + 'Queensland': EmojiStateCodes.auQueensland, + 'South Australia': EmojiStateCodes.auSouthAustralia, + 'Tasmania': EmojiStateCodes.auTasmania, + 'Victoria': EmojiStateCodes.auVictoria, + 'Western Australia': EmojiStateCodes.auWesternAustralia, + }; + + static const _indiaEnglish = { + 'Andaman and Nicobar Islands': EmojiStateCodes.inAndamanAndNicobarIslands, + 'Andhra Pradesh': EmojiStateCodes.inAndhraPradesh, + 'Arunachal Pradesh': EmojiStateCodes.inArunachalPradesh, + 'Assam': EmojiStateCodes.inAssam, + 'Bihar': EmojiStateCodes.inBihar, + 'Chandigarh': EmojiStateCodes.inChandigarh, + 'Chhattisgarh': EmojiStateCodes.inChhattisgarh, + 'Daman and Diu': EmojiStateCodes.inDamanAndDiu, + 'Delhi': EmojiStateCodes.inDelhi, + 'Dadra and Nagar Haveli': EmojiStateCodes.inDadraAndNagarHaveli, + 'Goa': EmojiStateCodes.inGoa, + 'Gujarat': EmojiStateCodes.inGujarat, + 'Himachal Pradesh': EmojiStateCodes.inHimachalPradesh, + 'Haryana': EmojiStateCodes.inHaryana, + 'Jharkhand': EmojiStateCodes.inJharkhand, + 'Jammu and Kashmir': EmojiStateCodes.inJammuAndKashmir, + 'Karnataka': EmojiStateCodes.inKarnataka, + 'Kerala': EmojiStateCodes.inKerala, + 'Lakshadweep': EmojiStateCodes.inLakshadweep, + 'Maharashtra': EmojiStateCodes.inMaharashtra, + 'Meghalaya': EmojiStateCodes.inMeghalaya, + 'Manipur': EmojiStateCodes.inManipur, + 'Madhya Pradesh': EmojiStateCodes.inMadhyaPradesh, + 'Mizoram': EmojiStateCodes.inMizoram, + 'Nagaland': EmojiStateCodes.inNagaland, + 'Odisha': EmojiStateCodes.inOdisha, + 'Punjab': EmojiStateCodes.inPunjab, + 'Puducherry': EmojiStateCodes.inPuducherry, + 'Rajasthan': EmojiStateCodes.inRajasthan, + 'Sikkim': EmojiStateCodes.inSikkim, + 'Telangana': EmojiStateCodes.inTelangana, + 'Tamil Nadu': EmojiStateCodes.inTamilNadu, + 'Tripura': EmojiStateCodes.inTripura, + 'Uttar Pradesh': EmojiStateCodes.inUttarPradesh, + 'Uttarakhand': EmojiStateCodes.inUttarakhand, + 'West Bengal': EmojiStateCodes.inWestBengal, + }; + + static const _unitedKingdomEnglish = { + 'England': EmojiStateCodes.gbEngland, + 'Northern Ireland': EmojiStateCodes.gbNorthernIreland, + 'Scotland': EmojiStateCodes.gbScotland, + 'Wales': EmojiStateCodes.gbWales, + }; + + static const _unitedStatesEnglish = { + 'Alabama': EmojiStateCodes.usAlabama, + 'Alaska': EmojiStateCodes.usAlaska, + 'Arizona': EmojiStateCodes.usArizona, + 'Arkansas': EmojiStateCodes.usArkansas, + 'California': EmojiStateCodes.usCalifornia, + 'Colorado': EmojiStateCodes.usColorado, + 'Connecticut': EmojiStateCodes.usConnecticut, + 'Delaware': EmojiStateCodes.usDelaware, + 'Florida': EmojiStateCodes.usFlorida, + 'Georgia': EmojiStateCodes.usGeorgia, + 'Hawaii': EmojiStateCodes.usHawaii, + 'Idaho': EmojiStateCodes.usIdaho, + 'Illinois': EmojiStateCodes.usIllinois, + 'Indiana': EmojiStateCodes.usIndiana, + 'Iowa': EmojiStateCodes.usIowa, + 'Kansas': EmojiStateCodes.usKansas, + 'Kentucky': EmojiStateCodes.usKentucky, + 'Louisiana': EmojiStateCodes.usLouisiana, + 'Maine': EmojiStateCodes.usMaine, + 'Maryland': EmojiStateCodes.usMaryland, + 'Massachusetts': EmojiStateCodes.usMassachusetts, + 'Michigan': EmojiStateCodes.usMichigan, + 'Minnesota': EmojiStateCodes.usMinnesota, + 'Mississippi': EmojiStateCodes.usMississippi, + 'Missouri': EmojiStateCodes.usMissouri, + 'Montana': EmojiStateCodes.usMontana, + 'Nebraska': EmojiStateCodes.usNebraska, + 'Nevada': EmojiStateCodes.usNevada, + 'New Hampshire': EmojiStateCodes.usNewHampshire, + 'New Jersey': EmojiStateCodes.usNewJersey, + 'New Mexico': EmojiStateCodes.usNewMexico, + 'New York': EmojiStateCodes.usNewYork, + 'North Carolina': EmojiStateCodes.usNorthCarolina, + 'North Dakota': EmojiStateCodes.usNorthDakota, + 'Ohio': EmojiStateCodes.usOhio, + 'Oklahoma': EmojiStateCodes.usOklahoma, + 'Oregon': EmojiStateCodes.usOregon, + 'Pennsylvania': EmojiStateCodes.usPennsylvania, + 'Rhode Island': EmojiStateCodes.usRhodeIsland, + 'South Carolina': EmojiStateCodes.usSouthCarolina, + 'South Dakota': EmojiStateCodes.usSouthDakota, + 'Tennessee': EmojiStateCodes.usTennessee, + 'Utah': EmojiStateCodes.usUtah, + 'Vermont': EmojiStateCodes.usVermont, + 'Virginia': EmojiStateCodes.usVirginia, + 'Washington': EmojiStateCodes.usWashington, + 'Washington DC': EmojiStateCodes.usWashingtonDC, + 'West Virginia': EmojiStateCodes.usWestVirginia, + 'Wisconsin': EmojiStateCodes.usWisconsin, + 'Wyoming': EmojiStateCodes.usWyoming, + }; +} diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart index 0729c7ce1..a0aaa32b2 100644 --- a/lib/image_providers/app_icon_image_provider.dart +++ b/lib/image_providers/app_icon_image_provider.dart @@ -39,7 +39,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> { Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async { try { - final bytes = await androidAppService.getAppIcon(key.packageName, key.size); + final bytes = await appService.getAppIcon(key.packageName, key.size); final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes); return await decode(buffer); } catch (error) { diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 826c8f5c4..c388db77d 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -1426,5 +1426,31 @@ "vaultDialogLockModeWhenScreenOff": "Uzamknout při vypnutí displeje", "@vaultDialogLockModeWhenScreenOff": {}, "vaultBinUsageDialogMessage": "Některé trezory používají koš.", - "@vaultBinUsageDialogMessage": {} + "@vaultBinUsageDialogMessage": {}, + "settingsVideoBackgroundMode": "Režim na pozadí", + "@settingsVideoBackgroundMode": {}, + "settingsCollectionBurstPatternsNone": "Žádný", + "@settingsCollectionBurstPatternsNone": {}, + "chipActionShowCountryStates": "Zobrazit země", + "@chipActionShowCountryStates": {}, + "viewerActionLock": "Uzamknout prohlížení", + "@viewerActionLock": {}, + "viewerActionUnlock": "Odemknout prohlížení", + "@viewerActionUnlock": {}, + "settingsVideoEnablePip": "Obraz v obraze", + "@settingsVideoEnablePip": {}, + "statePageTitle": "Státy", + "@statePageTitle": {}, + "stateEmpty": "Žádné státy", + "@stateEmpty": {}, + "searchStatesSectionTitle": "Státy", + "@searchStatesSectionTitle": {}, + "settingsCollectionBurstPatternsTile": "Vzory dávek", + "@settingsCollectionBurstPatternsTile": {}, + "statsTopStatesSectionTitle": "Nejčastější státy", + "@statsTopStatesSectionTitle": {}, + "tagPlaceholderState": "Stát", + "@tagPlaceholderState": {}, + "settingsVideoBackgroundModeDialogTitle": "Režim na pozadí", + "@settingsVideoBackgroundModeDialogTitle": {} } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 16361c294..9e83cf533 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1208,5 +1208,91 @@ "settingsModificationWarningDialogMessage": "Andere Einstellungen werden angepasst.", "@settingsModificationWarningDialogMessage": {}, "settingsViewerShowDescription": "Beschreibung anzeigen", - "@settingsViewerShowDescription": {} + "@settingsViewerShowDescription": {}, + "chipActionGoToPlacePage": "In Orten anzeigen", + "@chipActionGoToPlacePage": {}, + "chipActionLock": "Sperren", + "@chipActionLock": {}, + "chipActionCreateVault": "Tresor anlegen", + "@chipActionCreateVault": {}, + "chipActionConfigureVault": "Tresor konfigurieren", + "@chipActionConfigureVault": {}, + "settingsCollectionBurstPatternsTile": "Berstmuster", + "@settingsCollectionBurstPatternsTile": {}, + "settingsVideoEnablePip": "Bild-in-Bild", + "@settingsVideoEnablePip": {}, + "patternDialogEnter": "Muster eingeben", + "@patternDialogEnter": {}, + "tagPlaceholderState": "Staat", + "@tagPlaceholderState": {}, + "settingsDisablingBinWarningDialogMessage": "Die Elemente im Papierkorb werden für immer gelöscht.", + "@settingsDisablingBinWarningDialogMessage": {}, + "chipActionShowCountryStates": "Staaten anzeigen", + "@chipActionShowCountryStates": {}, + "viewerActionLock": "Anzeige sperren", + "@viewerActionLock": {}, + "viewerActionUnlock": "Anzeige entsperren", + "@viewerActionUnlock": {}, + "albumTierVaults": "Tresore", + "@albumTierVaults": {}, + "patternDialogConfirm": "Muster bestätigen", + "@patternDialogConfirm": {}, + "exportEntryDialogWriteMetadata": "Metadaten schreiben", + "@exportEntryDialogWriteMetadata": {}, + "drawerPlacePage": "Orte", + "@drawerPlacePage": {}, + "statePageTitle": "Staaten", + "@statePageTitle": {}, + "stateEmpty": "Keine Staaten", + "@stateEmpty": {}, + "placePageTitle": "Orte", + "@placePageTitle": {}, + "placeEmpty": "Keine Orte", + "@placeEmpty": {}, + "settingsCollectionBurstPatternsNone": "Nichts", + "@settingsCollectionBurstPatternsNone": {}, + "settingsVideoBackgroundMode": "Hintergrund-Modus", + "@settingsVideoBackgroundMode": {}, + "searchStatesSectionTitle": "Staaten", + "@searchStatesSectionTitle": {}, + "settingsConfirmationVaultDataLoss": "Warnung vor Tresordatenverlust anzeigen", + "@settingsConfirmationVaultDataLoss": {}, + "settingsVideoBackgroundModeDialogTitle": "Hintergrund-Modus", + "@settingsVideoBackgroundModeDialogTitle": {}, + "statsTopStatesSectionTitle": "Top Staaten", + "@statsTopStatesSectionTitle": {}, + "lengthUnitPercent": "%", + "@lengthUnitPercent": {}, + "lengthUnitPixel": "px", + "@lengthUnitPixel": {}, + "vaultLockTypePattern": "Muster", + "@vaultLockTypePattern": {}, + "vaultLockTypePassword": "Passwort", + "@vaultLockTypePassword": {}, + "vaultLockTypePin": "PIN", + "@vaultLockTypePin": {}, + "passwordDialogEnter": "Passwort eingeben", + "@passwordDialogEnter": {}, + "passwordDialogConfirm": "Passwort bestätigen", + "@passwordDialogConfirm": {}, + "authenticateToConfigureVault": "Authentifizierung zum Konfigurieren des Tresors", + "@authenticateToConfigureVault": {}, + "newVaultWarningDialogMessage": "Elemente in Tresoren sind nur für diese App verfügbar und nicht in anderen.\n\nWenn Sie diese App deinstallieren oder die Daten dieser App löschen, gehen alle diese Elemente verloren.", + "@newVaultWarningDialogMessage": {}, + "newVaultDialogTitle": "Neuer Tresor", + "@newVaultDialogTitle": {}, + "configureVaultDialogTitle": "Tresor konfigurieren", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "Sperren beim Ausschalten des Bildschirms", + "@vaultDialogLockModeWhenScreenOff": {}, + "vaultDialogLockTypeLabel": "Schloss-Typ", + "@vaultDialogLockTypeLabel": {}, + "pinDialogConfirm": "PIN bestätigen", + "@pinDialogConfirm": {}, + "authenticateToUnlockVault": "Authentifizierung zum Entsperren des Tresors", + "@authenticateToUnlockVault": {}, + "vaultBinUsageDialogMessage": "Einige Tresore verwenden den Papierkorb.", + "@vaultBinUsageDialogMessage": {}, + "pinDialogEnter": "PIN eingeben", + "@pinDialogEnter": {} } diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb index ec6cc71a5..4622bb15a 100644 --- a/lib/l10n/app_el.arb +++ b/lib/l10n/app_el.arb @@ -723,7 +723,7 @@ "@searchAlbumsSectionTitle": {}, "searchCountriesSectionTitle": "Χωρες", "@searchCountriesSectionTitle": {}, - "searchPlacesSectionTitle": "Τοποθεσιες", + "searchPlacesSectionTitle": "Μερη", "@searchPlacesSectionTitle": {}, "searchTagsSectionTitle": "Ετικετες", "@searchTagsSectionTitle": {}, @@ -1252,5 +1252,47 @@ "lengthUnitPercent": "%", "@lengthUnitPercent": {}, "lengthUnitPixel": "px", - "@lengthUnitPixel": {} + "@lengthUnitPixel": {}, + "chipActionGoToPlacePage": "Εμφάνιση στα μέρη", + "@chipActionGoToPlacePage": {}, + "patternDialogConfirm": "Επιβεβαιώστε το μοτίβο", + "@patternDialogConfirm": {}, + "drawerPlacePage": "Μέρη", + "@drawerPlacePage": {}, + "settingsVideoBackgroundMode": "Αναπαραγωγή στο παρασκήνιο", + "@settingsVideoBackgroundMode": {}, + "chipActionShowCountryStates": "Εμφάνιση πολιτειών", + "@chipActionShowCountryStates": {}, + "viewerActionLock": "Κλείδωμα προβολής", + "@viewerActionLock": {}, + "patternDialogEnter": "Εισάγετε το μοτίβο", + "@patternDialogEnter": {}, + "statePageTitle": "Πολιτειες", + "@statePageTitle": {}, + "stateEmpty": "Χωρίς πολιτεία", + "@stateEmpty": {}, + "searchStatesSectionTitle": "Πολιτειες", + "@searchStatesSectionTitle": {}, + "settingsCollectionBurstPatternsTile": "Εμφάνιση μοτίβων", + "@settingsCollectionBurstPatternsTile": {}, + "settingsCollectionBurstPatternsNone": "Χωρίς", + "@settingsCollectionBurstPatternsNone": {}, + "settingsVideoBackgroundModeDialogTitle": "Αναπαραγωγη στο παρασκηνιο", + "@settingsVideoBackgroundModeDialogTitle": {}, + "statsTopStatesSectionTitle": "Κορυφαιες Πολιτειες", + "@statsTopStatesSectionTitle": {}, + "tagPlaceholderState": "Πολιτεία", + "@tagPlaceholderState": {}, + "exportEntryDialogWriteMetadata": "Εγγραφή μεταδεδομένων", + "@exportEntryDialogWriteMetadata": {}, + "placePageTitle": "Μερη", + "@placePageTitle": {}, + "placeEmpty": "Χωρίς μέρος", + "@placeEmpty": {}, + "settingsVideoEnablePip": "Picture-in-picture", + "@settingsVideoEnablePip": {}, + "viewerActionUnlock": "Ξεκλείδωμα προβολής", + "@viewerActionUnlock": {}, + "vaultLockTypePattern": "Μοτίβο", + "@vaultLockTypePattern": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ad6d058b8..4bc25f2df 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -84,6 +84,7 @@ "chipActionUnpin": "Unpin from top", "chipActionRename": "Rename", "chipActionSetCover": "Set cover", + "chipActionShowCountryStates": "Show states", "chipActionCreateAlbum": "Create album", "chipActionCreateVault": "Create vault", "chipActionConfigureVault": "Configure vault", @@ -125,6 +126,8 @@ "videoActionSetSpeed": "Playback speed", "viewerActionSettings": "Settings", + "viewerActionLock": "Lock viewer", + "viewerActionUnlock": "Unlock viewer", "slideshowActionResume": "Resume", "slideshowActionShowInCollection": "Show in Collection", @@ -677,6 +680,9 @@ "countryPageTitle": "Countries", "countryEmpty": "No countries", + "statePageTitle": "States", + "stateEmpty": "No states", + "placePageTitle": "Places", "placeEmpty": "No places", @@ -690,6 +696,7 @@ "searchDateSectionTitle": "Date", "searchAlbumsSectionTitle": "Albums", "searchCountriesSectionTitle": "Countries", + "searchStatesSectionTitle": "States", "searchPlacesSectionTitle": "Places", "searchTagsSectionTitle": "Tags", "searchRatingSectionTitle": "Ratings", @@ -754,6 +761,9 @@ "settingsCollectionBrowsingQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when browsing items.", "settingsCollectionSelectionQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when selecting items.", + "settingsCollectionBurstPatternsTile": "Burst patterns", + "settingsCollectionBurstPatternsNone": "None", + "settingsViewerSectionTitle": "Viewer", "settingsViewerGestureSideTapNext": "Tap on screen edges to show previous/next item", "settingsViewerUseCutout": "Use cutout area", @@ -892,6 +902,7 @@ } }, "statsTopCountriesSectionTitle": "Top Countries", + "statsTopStatesSectionTitle": "Top States", "statsTopPlacesSectionTitle": "Top Places", "statsTopTagsSectionTitle": "Top Tags", "statsTopAlbumsSectionTitle": "Top Albums", @@ -948,6 +959,7 @@ "tagEditorSectionPlaceholders": "Placeholders", "tagPlaceholderCountry": "Country", + "tagPlaceholderState": "State", "tagPlaceholderPlace": "Place", "panoramaEnableSensorControl": "Enable sensor control", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 769e44b93..de81bf635 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1273,6 +1273,26 @@ "@settingsVideoEnablePip": {}, "settingsVideoBackgroundMode": "Reproducción de fondo", "@settingsVideoBackgroundMode": {}, - "settingsVideoBackgroundModeDialogTitle": "Background mode", - "@settingsVideoBackgroundModeDialogTitle": {} + "settingsVideoBackgroundModeDialogTitle": "Reproducción de fondo", + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsCollectionBurstPatternsTile": "Modelos de ráfaga", + "@settingsCollectionBurstPatternsTile": {}, + "settingsCollectionBurstPatternsNone": "Ninguno", + "@settingsCollectionBurstPatternsNone": {}, + "tagPlaceholderState": "Estado", + "@tagPlaceholderState": {}, + "viewerActionUnlock": "Desbloquear visor", + "@viewerActionUnlock": {}, + "stateEmpty": "Sin estados", + "@stateEmpty": {}, + "chipActionShowCountryStates": "Mostrar los estados", + "@chipActionShowCountryStates": {}, + "statePageTitle": "Estados", + "@statePageTitle": {}, + "viewerActionLock": "Bloquear visor", + "@viewerActionLock": {}, + "searchStatesSectionTitle": "Estados", + "@searchStatesSectionTitle": {}, + "statsTopStatesSectionTitle": "Estados principales", + "@statsTopStatesSectionTitle": {} } diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb index 5c587223b..e1929c0bf 100644 --- a/lib/l10n/app_eu.arb +++ b/lib/l10n/app_eu.arb @@ -1432,5 +1432,25 @@ "settingsVideoBackgroundMode": "Erreprodukzioa atzeko planoan", "@settingsVideoBackgroundMode": {}, "settingsVideoBackgroundModeDialogTitle": "Atzeko planoko modua", - "@settingsVideoBackgroundModeDialogTitle": {} + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsCollectionBurstPatternsNone": "Bat ere ez", + "@settingsCollectionBurstPatternsNone": {}, + "settingsCollectionBurstPatternsTile": "Segida moduak", + "@settingsCollectionBurstPatternsTile": {}, + "tagPlaceholderState": "Egoera", + "@tagPlaceholderState": {}, + "viewerActionUnlock": "Deskblokeatu bisorea", + "@viewerActionUnlock": {}, + "stateEmpty": "Egoerarik ez", + "@stateEmpty": {}, + "chipActionShowCountryStates": "Erakutsi egoerak", + "@chipActionShowCountryStates": {}, + "statePageTitle": "Egoerak", + "@statePageTitle": {}, + "viewerActionLock": "Blokeatu bisorea", + "@viewerActionLock": {}, + "searchStatesSectionTitle": "Egoerak", + "@searchStatesSectionTitle": {}, + "statsTopStatesSectionTitle": "Egoera Nagusiak", + "@statsTopStatesSectionTitle": {} } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 64035ab81..cc28f51e9 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1274,5 +1274,25 @@ "settingsVideoBackgroundMode": "Lecture en arrière-plan", "@settingsVideoBackgroundMode": {}, "settingsVideoBackgroundModeDialogTitle": "Arrière-plan", - "@settingsVideoBackgroundModeDialogTitle": {} + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsCollectionBurstPatternsNone": "Aucun", + "@settingsCollectionBurstPatternsNone": {}, + "settingsCollectionBurstPatternsTile": "Modèles de rafale", + "@settingsCollectionBurstPatternsTile": {}, + "tagPlaceholderState": "État", + "@tagPlaceholderState": {}, + "chipActionShowCountryStates": "Afficher les États", + "@chipActionShowCountryStates": {}, + "stateEmpty": "Aucun État", + "@stateEmpty": {}, + "searchStatesSectionTitle": "États", + "@searchStatesSectionTitle": {}, + "statePageTitle": "États", + "@statePageTitle": {}, + "statsTopStatesSectionTitle": "Top États", + "@statsTopStatesSectionTitle": {}, + "viewerActionLock": "Verrouiller la visionneuse", + "@viewerActionLock": {}, + "viewerActionUnlock": "Déverrouiller la visionneuse", + "@viewerActionUnlock": {} } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb new file mode 100644 index 000000000..071513ba8 --- /dev/null +++ b/lib/l10n/app_hi.arb @@ -0,0 +1,77 @@ +{ + "welcomeOptional": "वैकल्पिक", + "@welcomeOptional": {}, + "welcomeTermsToggle": "मैं नियमों और शर्तों पर सहमत हुं", + "@welcomeTermsToggle": {}, + "columnCount": "{count, plural, =1{१ कॉलम} other{{count} कॉलम}}", + "@columnCount": { + "placeholders": { + "count": {} + } + }, + "timeSeconds": "{seconds, plural, =1{ १ सेकंड} other{{seconds} सेकंडस}}", + "@timeSeconds": { + "placeholders": { + "seconds": {} + } + }, + "timeDays": "{days, plural, =1{ १ दिन} other{{days} दिन}}", + "@timeDays": { + "placeholders": { + "days": {} + } + }, + "applyButtonLabel": "लगाऐ", + "@applyButtonLabel": {}, + "nextButtonLabel": "आगे", + "@nextButtonLabel": {}, + "showButtonLabel": "देखे", + "@showButtonLabel": {}, + "hideButtonLabel": "छिपाए", + "@hideButtonLabel": {}, + "continueButtonLabel": "जारी रखे", + "@continueButtonLabel": {}, + "clearTooltip": "मिटाएं", + "@clearTooltip": {}, + "actionRemove": "हटाएं", + "@actionRemove": {}, + "itemCount": "{count, plural, =1{१ चीज} other{{count} चीजे}}", + "@itemCount": { + "placeholders": { + "count": {} + } + }, + "deleteButtonLabel": "डिलीट", + "@deleteButtonLabel": {}, + "timeMinutes": "{minutes, plural, =1{ १ मिनट} other{{minutes} मिनट}}", + "@timeMinutes": { + "placeholders": { + "minutes": {} + } + }, + "focalLength": "{length} एम एम", + "@focalLength": { + "placeholders": { + "length": { + "type": "String", + "example": "5.4" + } + } + }, + "nextTooltip": "आगे", + "@nextTooltip": {}, + "appName": "ऐवीज", + "@appName": {}, + "welcomeMessage": "ऐवीज मे आपका स्वागत है", + "@welcomeMessage": {}, + "previousTooltip": "पिछे", + "@previousTooltip": {}, + "hideTooltip": "छिपाए", + "@hideTooltip": {}, + "cancelTooltip": "कैंसिल", + "@cancelTooltip": {}, + "changeTooltip": "बदलें", + "@changeTooltip": {}, + "showTooltip": "देखें", + "@showTooltip": {} +} diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb new file mode 100644 index 000000000..dcf3fc14a --- /dev/null +++ b/lib/l10n/app_hu.arb @@ -0,0 +1,186 @@ +{ + "applyButtonLabel": "ALKALMAZ", + "@applyButtonLabel": {}, + "deleteButtonLabel": "TÖRLÉS", + "@deleteButtonLabel": {}, + "nextButtonLabel": "KÖVETKEZŐ", + "@nextButtonLabel": {}, + "continueButtonLabel": "FOLYTAT", + "@continueButtonLabel": {}, + "previousTooltip": "Előző", + "@previousTooltip": {}, + "nextTooltip": "Következő", + "@nextTooltip": {}, + "saveTooltip": "Mentés", + "@saveTooltip": {}, + "sourceStateLoading": "Betöltés", + "@sourceStateLoading": {}, + "doNotAskAgain": "Ne kérdezd újra", + "@doNotAskAgain": {}, + "chipActionDelete": "Törlés", + "@chipActionDelete": {}, + "appName": "Aves", + "@appName": {}, + "welcomeMessage": "Üdvözöl az Aves", + "@welcomeMessage": {}, + "cancelTooltip": "Mégse", + "@cancelTooltip": {}, + "chipActionCreateAlbum": "Új album", + "@chipActionCreateAlbum": {}, + "entryActionCopyToClipboard": "Vágolapra másolás", + "@entryActionCopyToClipboard": {}, + "entryActionDelete": "Törlés", + "@entryActionDelete": {}, + "entryActionExport": "Exportálás", + "@entryActionExport": {}, + "entryActionInfo": "Infó", + "@entryActionInfo": {}, + "entryActionShare": "Megosztás", + "@entryActionShare": {}, + "entryActionPrint": "Nyomtatás", + "@entryActionPrint": {}, + "entryActionEdit": "Szerkesztés", + "@entryActionEdit": {}, + "entryActionRotateScreen": "Képernyő forgatása", + "@entryActionRotateScreen": {}, + "entryActionAddFavourite": "Kedvencekhez adás", + "@entryActionAddFavourite": {}, + "videoActionMute": "Némítás", + "@videoActionMute": {}, + "viewerActionSettings": "Beállítások", + "@viewerActionSettings": {}, + "entryInfoActionEditDate": "Dátum és idő szerkesztése", + "@entryInfoActionEditDate": {}, + "filterNoTitleLabel": "Névtelen", + "@filterNoTitleLabel": {}, + "filterOnThisDayLabel": "Ezen a napon", + "@filterOnThisDayLabel": {}, + "filterRecentlyAddedLabel": "Nemrég hozzáadva", + "@filterRecentlyAddedLabel": {}, + "filterTypePanoramaLabel": "Panoráma", + "@filterTypePanoramaLabel": {}, + "filterMimeVideoLabel": "Videó", + "@filterMimeVideoLabel": {}, + "albumTierNew": "Új", + "@albumTierNew": {}, + "themeBrightnessDark": "Sötét", + "@themeBrightnessDark": {}, + "vaultLockTypePassword": "Jelszó", + "@vaultLockTypePassword": {}, + "videoControlsPlay": "Lejátszás", + "@videoControlsPlay": {}, + "videoControlsNone": "Nincs", + "@videoControlsNone": {}, + "videoLoopModeAlways": "Mindig", + "@videoLoopModeAlways": {}, + "viewerTransitionNone": "Nincs", + "@viewerTransitionNone": {}, + "storageVolumeDescriptionFallbackPrimary": "Belső tárhely", + "@storageVolumeDescriptionFallbackPrimary": {}, + "storageVolumeDescriptionFallbackNonPrimary": "SD kártya", + "@storageVolumeDescriptionFallbackNonPrimary": {}, + "newAlbumDialogTitle": "Új album", + "@newAlbumDialogTitle": {}, + "newAlbumDialogNameLabel": "Album neve", + "@newAlbumDialogNameLabel": {}, + "newAlbumDialogNameLabelAlreadyExistsHelper": "A mappa már létezik", + "@newAlbumDialogNameLabelAlreadyExistsHelper": {}, + "newAlbumDialogStorageLabel": "Tárhely:", + "@newAlbumDialogStorageLabel": {}, + "renameAlbumDialogLabel": "Új név", + "@renameAlbumDialogLabel": {}, + "renameAlbumDialogLabelAlreadyExistsHelper": "A mappa már létezik", + "@renameAlbumDialogLabelAlreadyExistsHelper": {}, + "renameEntrySetPageTitle": "Átnevezés", + "@renameEntrySetPageTitle": {}, + "renameProcessorName": "Név", + "@renameProcessorName": {}, + "renameEntryDialogLabel": "Új név", + "@renameEntryDialogLabel": {}, + "editEntryDateDialogTitle": "Dátum és idő", + "@editEntryDateDialogTitle": {}, + "videoStreamSelectionDialogText": "Feliratok", + "@videoStreamSelectionDialogText": {}, + "videoStreamSelectionDialogOff": "Ki", + "@videoStreamSelectionDialogOff": {}, + "genericSuccessFeedback": "Kész!", + "@genericSuccessFeedback": {}, + "genericFailureFeedback": "Sikertelen", + "@genericFailureFeedback": {}, + "genericDangerWarningDialogMessage": "Biztos benne?", + "@genericDangerWarningDialogMessage": {}, + "menuActionSlideshow": "Diavetités", + "@menuActionSlideshow": {}, + "coverDialogTabCover": "Borító", + "@coverDialogTabCover": {}, + "appPickDialogNone": "Nincs", + "@appPickDialogNone": {}, + "aboutPageTitle": "Névjegy", + "@aboutPageTitle": {}, + "aboutLinkLicense": "Licensz", + "@aboutLinkLicense": {}, + "aboutBugSectionTitle": "Hiba jelentés", + "@aboutBugSectionTitle": {}, + "aboutBugCopyInfoButton": "Másolás", + "@aboutBugCopyInfoButton": {}, + "aboutTranslatorsSectionTitle": "Fordítók", + "@aboutTranslatorsSectionTitle": {}, + "collectionActionEdit": "Szerkesztés", + "@collectionActionEdit": {}, + "dateToday": "Ma", + "@dateToday": {}, + "dateThisMonth": "Ebben a hónapban", + "@dateThisMonth": {}, + "drawerAboutButton": "Névjegy", + "@drawerAboutButton": {}, + "drawerSettingsButton": "Beállítások", + "@drawerSettingsButton": {}, + "drawerCollectionFavourites": "Kedvencek", + "@drawerCollectionFavourites": {}, + "drawerCollectionImages": "Képek", + "@drawerCollectionImages": {}, + "drawerCollectionVideos": "Videók", + "@drawerCollectionVideos": {}, + "drawerCollectionPanoramas": "Panorámák", + "@drawerCollectionPanoramas": {}, + "albumDownload": "Letöltés", + "@albumDownload": {}, + "albumScreenshots": "Képernyő képek", + "@albumScreenshots": {}, + "albumPageTitle": "Albumok", + "@albumPageTitle": {}, + "newFilterBanner": "új", + "@newFilterBanner": {}, + "chipActionRename": "Átnevez", + "@chipActionRename": {}, + "entryActionRename": "Átnevezés", + "@entryActionRename": {}, + "keepScreenOnNever": "Soha", + "@keepScreenOnNever": {}, + "videoLoopModeNever": "Soha", + "@videoLoopModeNever": {}, + "videoActionPlay": "Lejátszás", + "@videoActionPlay": {}, + "entryInfoActionRemoveMetadata": "Metaadat eltávolítása", + "@entryInfoActionRemoveMetadata": {}, + "albumTierRegular": "Egyebek", + "@albumTierRegular": {}, + "keepScreenOnAlways": "Mindig", + "@keepScreenOnAlways": {}, + "nameConflictStrategyRename": "Átnevezés", + "@nameConflictStrategyRename": {}, + "themeBrightnessBlack": "Fekete", + "@themeBrightnessBlack": {}, + "menuActionMap": "Térkép", + "@menuActionMap": {}, + "collectionPageTitle": "Gyűjtemény", + "@collectionPageTitle": {}, + "sectionUnknown": "Ismeretlen", + "@sectionUnknown": {}, + "dateYesterday": "Tegnap", + "@dateYesterday": {}, + "drawerAlbumPage": "Albumok", + "@drawerAlbumPage": {}, + "albumCamera": "Kamera", + "@albumCamera": {} +} diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index b5037a10a..e6694b165 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -1274,5 +1274,25 @@ "settingsVideoBackgroundMode": "Mode latar belakang", "@settingsVideoBackgroundMode": {}, "settingsVideoBackgroundModeDialogTitle": "Mode Latar Belakang", - "@settingsVideoBackgroundModeDialogTitle": {} + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsCollectionBurstPatternsTile": "Pola semburan", + "@settingsCollectionBurstPatternsTile": {}, + "settingsCollectionBurstPatternsNone": "Tidak ada", + "@settingsCollectionBurstPatternsNone": {}, + "chipActionShowCountryStates": "Tampilkan wilayah", + "@chipActionShowCountryStates": {}, + "viewerActionUnlock": "Buka kunci penampil", + "@viewerActionUnlock": {}, + "statePageTitle": "Wilayah", + "@statePageTitle": {}, + "stateEmpty": "Tidak ada wilayah", + "@stateEmpty": {}, + "tagPlaceholderState": "Wilayah", + "@tagPlaceholderState": {}, + "viewerActionLock": "Kunci penampil", + "@viewerActionLock": {}, + "searchStatesSectionTitle": "Wilayah", + "@searchStatesSectionTitle": {}, + "statsTopStatesSectionTitle": "Wilayah Teratas", + "@statsTopStatesSectionTitle": {} } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d2dc5001a..735dbd0e7 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -65,7 +65,7 @@ "@sourceStateLocatingPlaces": {}, "chipActionDelete": "Elimina", "@chipActionDelete": {}, - "chipActionGoToAlbumPage": "Mostra negli album", + "chipActionGoToAlbumPage": "Mostra negli Album", "@chipActionGoToAlbumPage": {}, "chipActionGoToCountryPage": "Mostra nei Paesi", "@chipActionGoToCountryPage": {}, @@ -1101,7 +1101,7 @@ "@viewerInfoOpenLinkText": {}, "viewerInfoViewXmlLinkText": "Visualizza XML", "@viewerInfoViewXmlLinkText": {}, - "viewerInfoSearchFieldLabel": "Metadati di ricerca", + "viewerInfoSearchFieldLabel": "Ricerca metadati", "@viewerInfoSearchFieldLabel": {}, "viewerInfoSearchEmpty": "Nessuna chiave corrispondente", "@viewerInfoSearchEmpty": {}, @@ -1248,5 +1248,49 @@ "settingsDisablingBinWarningDialogMessage": "Gli elementi nel cestino verranno eliminati permanentemente.", "@settingsDisablingBinWarningDialogMessage": {}, "configureVaultDialogTitle": "Configura Cassaforte", - "@configureVaultDialogTitle": {} + "@configureVaultDialogTitle": {}, + "exportEntryDialogWriteMetadata": "Scrivi metadati", + "@exportEntryDialogWriteMetadata": {}, + "chipActionGoToPlacePage": "Mostra nei Luoghi", + "@chipActionGoToPlacePage": {}, + "lengthUnitPercent": "%", + "@lengthUnitPercent": {}, + "lengthUnitPixel": "px", + "@lengthUnitPixel": {}, + "patternDialogEnter": "Inserisci sequenza", + "@patternDialogEnter": {}, + "patternDialogConfirm": "Conferma sequenza", + "@patternDialogConfirm": {}, + "drawerPlacePage": "Luoghi", + "@drawerPlacePage": {}, + "placeEmpty": "Nessun luogo", + "@placeEmpty": {}, + "placePageTitle": "Luoghi", + "@placePageTitle": {}, + "settingsVideoBackgroundMode": "Modalità sottofondo", + "@settingsVideoBackgroundMode": {}, + "settingsVideoBackgroundModeDialogTitle": "Modalità Sottofondo", + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsVideoEnablePip": "Picture-in-picture", + "@settingsVideoEnablePip": {}, + "vaultLockTypePattern": "Sequenza", + "@vaultLockTypePattern": {}, + "viewerActionLock": "Blocca visualizzazione", + "@viewerActionLock": {}, + "viewerActionUnlock": "Sblocca visualizzazione", + "@viewerActionUnlock": {}, + "statsTopStatesSectionTitle": "Stati più frequenti", + "@statsTopStatesSectionTitle": {}, + "tagPlaceholderState": "Stato", + "@tagPlaceholderState": {}, + "settingsCollectionBurstPatternsNone": "Nessuno", + "@settingsCollectionBurstPatternsNone": {}, + "chipActionShowCountryStates": "Mostra stati", + "@chipActionShowCountryStates": {}, + "statePageTitle": "Stati", + "@statePageTitle": {}, + "stateEmpty": "Nessuno stato", + "@stateEmpty": {}, + "searchStatesSectionTitle": "Stati", + "@searchStatesSectionTitle": {} } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index d909c5baf..ed47700b9 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1170,5 +1170,69 @@ "settingsSubtitleThemeTextPositionTile": "テキストの位置", "@settingsSubtitleThemeTextPositionTile": {}, "entryInfoActionExportMetadata": "メタデータをエクスポート", - "@entryInfoActionExportMetadata": {} + "@entryInfoActionExportMetadata": {}, + "subtitlePositionTop": "トップ", + "@subtitlePositionTop": {}, + "configureVaultDialogTitle": "保管庫の設定", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "画面オフ時にロック", + "@vaultDialogLockModeWhenScreenOff": {}, + "newVaultDialogTitle": "新しい保管庫", + "@newVaultDialogTitle": {}, + "authenticateToConfigureVault": "保管庫を設定するための認証", + "@authenticateToConfigureVault": {}, + "vaultDialogLockTypeLabel": "ロックの種類", + "@vaultDialogLockTypeLabel": {}, + "pinDialogEnter": "PINを入力", + "@pinDialogEnter": {}, + "patternDialogEnter": "パターンを入力", + "@patternDialogEnter": {}, + "pinDialogConfirm": "PINの確認", + "@pinDialogConfirm": {}, + "passwordDialogEnter": "パスワードを入力", + "@passwordDialogEnter": {}, + "authenticateToUnlockVault": "認証して保管庫のロックを解除する", + "@authenticateToUnlockVault": {}, + "passwordDialogConfirm": "パスワードの確認", + "@passwordDialogConfirm": {}, + "chipActionFilterIn": "フィルター", + "@chipActionFilterIn": {}, + "filterAspectRatioPortraitLabel": "縦向き", + "@filterAspectRatioPortraitLabel": {}, + "filterNoAddressLabel": "位置情報なし", + "@filterNoAddressLabel": {}, + "keepScreenOnVideoPlayback": "動画再生時", + "@keepScreenOnVideoPlayback": {}, + "chipActionGoToPlacePage": "場所別に表示", + "@chipActionGoToPlacePage": {}, + "tagPlaceholderState": "州", + "@tagPlaceholderState": {}, + "vaultLockTypePassword": "パスワード", + "@vaultLockTypePassword": {}, + "tooManyItemsErrorDialogMessage": "少ないアイテムで再度試してください。", + "@tooManyItemsErrorDialogMessage": {}, + "statePageTitle": "州", + "@statePageTitle": {}, + "drawerPlacePage": "場所", + "@drawerPlacePage": {}, + "chipActionLock": "ロック", + "@chipActionLock": {}, + "filterAspectRatioLandscapeLabel": "横向き", + "@filterAspectRatioLandscapeLabel": {}, + "vaultLockTypePin": "PIN", + "@vaultLockTypePin": {}, + "newVaultWarningDialogMessage": "保管庫のアイテムはアプリ内のみで保存しているため、他のアプリでは利用できません。\n\nこのアプリをアンインストールしたり、データを消去したりすると、これらのアイテムはすべて失われます。", + "@newVaultWarningDialogMessage": {}, + "patternDialogConfirm": "パターンの確認", + "@patternDialogConfirm": {}, + "placePageTitle": "場所", + "@placePageTitle": {}, + "settingsVideoEnablePip": "ピクチャインピクチャ", + "@settingsVideoEnablePip": {}, + "vaultLockTypePattern": "パターン", + "@vaultLockTypePattern": {}, + "lengthUnitPixel": "px", + "@lengthUnitPixel": {}, + "lengthUnitPercent": "%", + "@lengthUnitPercent": {} } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index a6bd96510..7eb29a4d4 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1274,5 +1274,25 @@ "settingsVideoBackgroundMode": "백그라운드 재생", "@settingsVideoBackgroundMode": {}, "settingsVideoBackgroundModeDialogTitle": "백그라운드 재생", - "@settingsVideoBackgroundModeDialogTitle": {} + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsCollectionBurstPatternsNone": "없음", + "@settingsCollectionBurstPatternsNone": {}, + "settingsCollectionBurstPatternsTile": "연속 촬영 양식", + "@settingsCollectionBurstPatternsTile": {}, + "tagPlaceholderState": "주", + "@tagPlaceholderState": {}, + "chipActionShowCountryStates": "주 보기", + "@chipActionShowCountryStates": {}, + "stateEmpty": "주가 없습니다", + "@stateEmpty": {}, + "searchStatesSectionTitle": "주", + "@searchStatesSectionTitle": {}, + "statsTopStatesSectionTitle": "주 랭킹", + "@statsTopStatesSectionTitle": {}, + "statePageTitle": "주", + "@statePageTitle": {}, + "viewerActionLock": "뷰어 잠금", + "@viewerActionLock": {}, + "viewerActionUnlock": "뷰어 잠금 해제", + "@viewerActionUnlock": {} } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 017cba23d..08173de71 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -517,7 +517,7 @@ "@aboutCreditsWorldAtlas1": {}, "aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.", "@aboutCreditsWorldAtlas2": {}, - "aboutTranslatorsSectionTitle": "Vdertalers", + "aboutTranslatorsSectionTitle": "Vertalers", "@aboutTranslatorsSectionTitle": {}, "aboutLicensesSectionTitle": "Open-Source Licenties", "@aboutLicensesSectionTitle": {}, @@ -1154,5 +1154,11 @@ "settingsAllowMediaManagement": "Mediabeheer toestaan", "@settingsAllowMediaManagement": {}, "editEntryLocationDialogSetCustom": "Aangepaste locatie instellen", - "@editEntryLocationDialogSetCustom": {} + "@editEntryLocationDialogSetCustom": {}, + "entryInfoActionExportMetadata": "Metagegevens exporteren", + "@entryInfoActionExportMetadata": {}, + "lengthUnitPercent": "%", + "@lengthUnitPercent": {}, + "vaultLockTypePin": "PIN", + "@vaultLockTypePin": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index b8fa68f67..ce2b9ab26 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1432,5 +1432,25 @@ "settingsVideoBackgroundMode": "Tryb tła", "@settingsVideoBackgroundMode": {}, "settingsVideoBackgroundModeDialogTitle": "Tryb tła", - "@settingsVideoBackgroundModeDialogTitle": {} + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsCollectionBurstPatternsNone": "Brak", + "@settingsCollectionBurstPatternsNone": {}, + "settingsCollectionBurstPatternsTile": "Wzory wybuchowe", + "@settingsCollectionBurstPatternsTile": {}, + "viewerActionUnlock": "Odblokuj przeglądarkę", + "@viewerActionUnlock": {}, + "viewerActionLock": "Zablokuj przeglądarkę", + "@viewerActionLock": {}, + "statePageTitle": "Stany", + "@statePageTitle": {}, + "stateEmpty": "Brak stanów", + "@stateEmpty": {}, + "searchStatesSectionTitle": "Stany", + "@searchStatesSectionTitle": {}, + "statsTopStatesSectionTitle": "Najpopularniejsze stany", + "@statsTopStatesSectionTitle": {}, + "tagPlaceholderState": "Stan", + "@tagPlaceholderState": {}, + "chipActionShowCountryStates": "Pokaż stany", + "@chipActionShowCountryStates": {} } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 5bc7f5726..7e379a006 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1271,6 +1271,28 @@ "@vaultLockTypePattern": {}, "settingsVideoEnablePip": "Picture-in-picture", "@settingsVideoEnablePip": {}, - "settingsVideoBackgroundMode": "Modo background", - "@settingsVideoBackgroundMode": {} + "settingsVideoBackgroundMode": "Modo de fundo", + "@settingsVideoBackgroundMode": {}, + "settingsCollectionBurstPatternsTile": "Padrões de explosão", + "@settingsCollectionBurstPatternsTile": {}, + "chipActionShowCountryStates": "Mostrar estados", + "@chipActionShowCountryStates": {}, + "viewerActionLock": "Bloquear visualizador", + "@viewerActionLock": {}, + "statePageTitle": "Estados", + "@statePageTitle": {}, + "stateEmpty": "Nenhum estado", + "@stateEmpty": {}, + "tagPlaceholderState": "Estado", + "@tagPlaceholderState": {}, + "searchStatesSectionTitle": "Estados", + "@searchStatesSectionTitle": {}, + "settingsCollectionBurstPatternsNone": "Nenhum", + "@settingsCollectionBurstPatternsNone": {}, + "statsTopStatesSectionTitle": "Principais Estados", + "@statsTopStatesSectionTitle": {}, + "viewerActionUnlock": "Desbloquear visualizador", + "@viewerActionUnlock": {}, + "settingsVideoBackgroundModeDialogTitle": "Modo de fundo", + "@settingsVideoBackgroundModeDialogTitle": {} } diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index 38330dc18..b892b5a0f 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -1406,5 +1406,51 @@ "newVaultWarningDialogMessage": "Elementele din seifuri sunt disponibile doar pentru această aplicație și nu pentru altele.\n\nDacă dezinstalezi această aplicație sau ștergi datele acestei aplicații, vei pierde toate aceste elemente.", "@newVaultWarningDialogMessage": {}, "settingsConfirmationVaultDataLoss": "Afișare avertisment privind pierderile de date din seif", - "@settingsConfirmationVaultDataLoss": {} + "@settingsConfirmationVaultDataLoss": {}, + "settingsVideoBackgroundModeDialogTitle": "Mod fundal", + "@settingsVideoBackgroundModeDialogTitle": {}, + "lengthUnitPixel": "px", + "@lengthUnitPixel": {}, + "exportEntryDialogWriteMetadata": "Scrierea metadatelor", + "@exportEntryDialogWriteMetadata": {}, + "drawerPlacePage": "Locații", + "@drawerPlacePage": {}, + "placePageTitle": "Locații", + "@placePageTitle": {}, + "lengthUnitPercent": "%", + "@lengthUnitPercent": {}, + "settingsVideoBackgroundMode": "Mod fundal", + "@settingsVideoBackgroundMode": {}, + "patternDialogEnter": "Introdu modelul", + "@patternDialogEnter": {}, + "patternDialogConfirm": "Confirmă modelul", + "@patternDialogConfirm": {}, + "placeEmpty": "Nu există locații", + "@placeEmpty": {}, + "settingsVideoEnablePip": "Imagine în imagine", + "@settingsVideoEnablePip": {}, + "vaultLockTypePattern": "Model", + "@vaultLockTypePattern": {}, + "chipActionGoToPlacePage": "Arată în Locuri", + "@chipActionGoToPlacePage": {}, + "settingsCollectionBurstPatternsNone": "Niciunul", + "@settingsCollectionBurstPatternsNone": {}, + "settingsCollectionBurstPatternsTile": "Modele de rafale", + "@settingsCollectionBurstPatternsTile": {}, + "tagPlaceholderState": "Stat", + "@tagPlaceholderState": {}, + "chipActionShowCountryStates": "Afișare state", + "@chipActionShowCountryStates": {}, + "viewerActionLock": "Blocarea vizualizatorului", + "@viewerActionLock": {}, + "viewerActionUnlock": "Deblocare vizualizator", + "@viewerActionUnlock": {}, + "statePageTitle": "State", + "@statePageTitle": {}, + "stateEmpty": "Nu există state", + "@stateEmpty": {}, + "searchStatesSectionTitle": "State", + "@searchStatesSectionTitle": {}, + "statsTopStatesSectionTitle": "Statele de top", + "@statsTopStatesSectionTitle": {} } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index aec8a7e4e..5e4fbf0e3 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1235,7 +1235,7 @@ "@filterLocatedLabel": {}, "filterTaggedLabel": "С тэгами", "@filterTaggedLabel": {}, - "chipActionGoToPlacePage": "Показать в местах", + "chipActionGoToPlacePage": "Показать в локациях", "@chipActionGoToPlacePage": {}, "settingsModificationWarningDialogMessage": "Другие настройки будут изменены.", "@settingsModificationWarningDialogMessage": {}, @@ -1244,5 +1244,23 @@ "settingsDisablingBinWarningDialogMessage": "Элементы в корзине будут удалены навсегда.", "@settingsDisablingBinWarningDialogMessage": {}, "lengthUnitPixel": "пикс.", - "@lengthUnitPixel": {} + "@lengthUnitPixel": {}, + "chipActionLock": "Заблокировать", + "@chipActionLock": {}, + "patternDialogEnter": "Введите ключ", + "@patternDialogEnter": {}, + "patternDialogConfirm": "Подтвердите ключ", + "@patternDialogConfirm": {}, + "vaultLockTypePattern": "Графический ключ", + "@vaultLockTypePattern": {}, + "drawerPlacePage": "Локации", + "@drawerPlacePage": {}, + "settingsVideoBackgroundMode": "Фоновый режим", + "@settingsVideoBackgroundMode": {}, + "settingsVideoBackgroundModeDialogTitle": "Фоновый режим", + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsVideoEnablePip": "Картинка в картинке", + "@settingsVideoEnablePip": {}, + "placeEmpty": "Нет локаций", + "@placeEmpty": {} } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 13b90185d..a09e87d79 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1432,5 +1432,25 @@ "settingsVideoBackgroundMode": "Фоновий режим", "@settingsVideoBackgroundMode": {}, "settingsVideoBackgroundModeDialogTitle": "Фоновий режим", - "@settingsVideoBackgroundModeDialogTitle": {} + "@settingsVideoBackgroundModeDialogTitle": {}, + "tagPlaceholderState": "Штат", + "@tagPlaceholderState": {}, + "chipActionShowCountryStates": "Показати штати", + "@chipActionShowCountryStates": {}, + "viewerActionUnlock": "Розблокувати переглядач", + "@viewerActionUnlock": {}, + "viewerActionLock": "Заблокувати переглядач", + "@viewerActionLock": {}, + "stateEmpty": "Немає штатів", + "@stateEmpty": {}, + "settingsCollectionBurstPatternsTile": "Вибух візерунків", + "@settingsCollectionBurstPatternsTile": {}, + "settingsCollectionBurstPatternsNone": "Нічого", + "@settingsCollectionBurstPatternsNone": {}, + "statsTopStatesSectionTitle": "Топ штатів", + "@statsTopStatesSectionTitle": {}, + "searchStatesSectionTitle": "Штати", + "@searchStatesSectionTitle": {}, + "statePageTitle": "Штати", + "@statePageTitle": {} } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 2acc34657..5db846beb 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1192,5 +1192,13 @@ "filterNoAddressLabel": "无地址", "@filterNoAddressLabel": {}, "settingsViewerShowRatingTags": "显示评分和标签", - "@settingsViewerShowRatingTags": {} + "@settingsViewerShowRatingTags": {}, + "chipActionLock": "锁定", + "@chipActionLock": {}, + "chipActionConfigureVault": "配置保险库", + "@chipActionConfigureVault": {}, + "chipActionCreateVault": "创建保险库", + "@chipActionCreateVault": {}, + "chipActionShowCountryStates": "显示状态", + "@chipActionShowCountryStates": {} } diff --git a/lib/model/actions/move_type.dart b/lib/model/actions/move_type.dart deleted file mode 100644 index cc7ff0c6b..000000000 --- a/lib/model/actions/move_type.dart +++ /dev/null @@ -1 +0,0 @@ -enum MoveType { copy, move, export, toBin, fromBin } diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart new file mode 100644 index 000000000..83a4ac43b --- /dev/null +++ b/lib/model/app/contributors.dart @@ -0,0 +1,64 @@ +class Contributors { + static const translators = { + Contributor('D3ZOXY', 'its.ghost.message@gmail.com'), + Contributor('JanWaldhorn', 'weblate@jwh.anonaddy.com'), + Contributor('n-berenice', null), + Contributor('Jonatas de Almeida Barros', 'ajonatas56@gmail.com'), + Contributor('MeFinity', 'me.dot.finity@gmail.com'), + Contributor('Maki', null), + Contributor('HiSubway', 'shenyusoftware@gmail.com'), + Contributor('glemco', 'glemco@posteo.net'), + Contributor('Aerowolf', null), + Contributor('小默', 'duzhe163908@gmail.com'), + Contributor('metezd', 'itoldyouthat@protonmail.com'), + Contributor('Martijn Fabrie', null), + Contributor('Koen Koppens', 'koenkoppens@proton.me'), + Contributor('Emmanouil Papavergis', null), + Contributor('kha84', 'khalukhin@gmail.com'), + Contributor('gallegonovato', 'fran-carro@hotmail.es'), + Contributor('Havokdan', 'havokdan@yahoo.com.br'), + Contributor('Jean Mareilles', 'waged1266@tutanota.com'), + Contributor('이정희', 'daemul72@gmail.com'), + Contributor('Translator-3000', 'weblate.m1d0h@8shield.net'), + Contributor('Ralea Adrian Vicențiu', 'ralea.adrian@gmail.com'), + Contributor('Igor Sorocean', 'sorocean.igor@gmail.com'), + Contributor('JY3', 'GeeyunJY3@gmail.com'), + Contributor('Gediminas Murauskas', 'muziejusinfo@gmail.com'), + Contributor('Oğuz Ersen', 'oguz@ersen.moe'), + Contributor('Allan Nordhøy', 'epost@anotheragency.no'), + Contributor('pemibe', 'pemibe4634@dmonies.com'), + Contributor('Linerly', 'linerly@protonmail.com'), + Contributor('Skrripy', 'rozihrash.ya6w7@simplelogin.com'), + Contributor('vesp', 'vesp@post.cz'), + Contributor('Dan', 'denqwerta@gmail.com'), + Contributor('Tijolinho', 'pedrohenrique29.alfenas@gmail.com'), + Contributor('Piotr K', '1337.kelt@gmail.com'), + Contributor('rehork', 'cooky@e.email'), + Contributor('Eric', 'hamburger2048@users.noreply.hosted.weblate.org'), + Contributor('Aitor Salaberria', 'trslbrr@gmail.com'), + Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'), + Contributor('kaajjo', 'claymanoff@gmail.com'), + Contributor('Eduardo Malaspina', 'vaio0@swismail.com'), + Contributor('Evgeniy Khramov', 'thejenjagamertjg@gmail.com'), + Contributor('syu_pf_ssy', 'syu.pf.ssy@outlook.com'), + Contributor('Dick Pluim', 'github@dickpluim.com'), + // Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic + // Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic + // Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian + // Contributor('slasb37', 'p84haghi@gmail.com'), // Persian + // Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk + // Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai + // Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew + // Contributor('Martin Frandel', 'martinko.fr@gmail.com'), // Slovak + // Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central) + // Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi + // Contributor('György Viktor', 'wickdj@gmail.com'), // Hungarian + }; +} + +class Contributor { + final String name; + final String? weblateEmail; + + const Contributor(this.name, this.weblateEmail); +} diff --git a/lib/utils/dependencies.dart b/lib/model/app/dependencies.dart similarity index 97% rename from lib/utils/dependencies.dart rename to lib/model/app/dependencies.dart index 0b33a9f47..dedcba43d 100644 --- a/lib/utils/dependencies.dart +++ b/lib/model/app/dependencies.dart @@ -112,12 +112,6 @@ class Dependencies { license: mit, sourceUrl: 'https://github.com/aaassseee/screen_brightness', ), - Dependency( - name: 'Screen State', - license: mit, - licenseUrl: 'https://github.com/cph-cachet/flutter-plugins/blob/master/packages/screen_state/LICENSE', - sourceUrl: 'https://github.com/cph-cachet/flutter-plugins/tree/master/packages/screen_state', - ), Dependency( name: 'Shared Preferences', license: bsd3, diff --git a/lib/model/app/permissions.dart b/lib/model/app/permissions.dart new file mode 100644 index 000000000..310427242 --- /dev/null +++ b/lib/model/app/permissions.dart @@ -0,0 +1,16 @@ +import 'package:permission_handler/permission_handler.dart'; + +class Permissions { + static const storage = [ + Permission.storage, + // for media access on Android >=13 + Permission.photos, + Permission.videos, + ]; + + static const mediaAccess = [ + ...storage, + // to access media with unredacted metadata with scoped storage (Android >=10) + Permission.accessMediaLocation, + ]; +} diff --git a/lib/model/app/support.dart b/lib/model/app/support.dart new file mode 100644 index 000000000..fbf2a29b4 --- /dev/null +++ b/lib/model/app/support.dart @@ -0,0 +1,96 @@ +import 'package:aves/ref/mime_types.dart'; + +class AppSupport { + // TODO TLAD [codec] make it dynamic if it depends on OS/lib versions + static const Set<String> undecodableImages = { + MimeTypes.art, + MimeTypes.cdr, + MimeTypes.crw, + MimeTypes.djvu, + MimeTypes.jpeg2000, + MimeTypes.jxl, + MimeTypes.pat, + MimeTypes.pcx, + MimeTypes.pnm, + MimeTypes.psdVnd, + MimeTypes.psdX, + MimeTypes.octetStream, + MimeTypes.zip, + }; + + static bool canDecode(String mimeType) => !undecodableImages.contains(mimeType); + + // Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported" + // but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below, + // and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested. + static bool _supportedByBitmapRegionDecoder(String mimeType) => [ + MimeTypes.heic, + MimeTypes.heif, + MimeTypes.jpeg, + MimeTypes.png, + MimeTypes.webp, + MimeTypes.arw, + MimeTypes.cr2, + MimeTypes.nef, + MimeTypes.nrw, + MimeTypes.orf, + MimeTypes.pef, + MimeTypes.raf, + MimeTypes.rw2, + MimeTypes.srw, + ].contains(mimeType); + + static bool canDecodeRegion(String mimeType) => _supportedByBitmapRegionDecoder(mimeType) || mimeType == MimeTypes.tiff; + + // `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes, + // and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files. + static bool canEditExif(String mimeType) { + switch (mimeType.toLowerCase()) { + // as of androidx.exifinterface:exifinterface:1.3.4 + case MimeTypes.jpeg: + case MimeTypes.png: + case MimeTypes.webp: + return true; + default: + return false; + } + } + + static bool canEditIptc(String mimeType) { + switch (mimeType.toLowerCase()) { + // as of latest PixyMeta + case MimeTypes.jpeg: + case MimeTypes.tiff: + return true; + default: + return false; + } + } + + static bool canEditXmp(String mimeType) { + switch (mimeType.toLowerCase()) { + // as of latest PixyMeta + case MimeTypes.gif: + case MimeTypes.jpeg: + case MimeTypes.png: + case MimeTypes.tiff: + return true; + // using `mp4parser` + case MimeTypes.mp4: + return true; + default: + return false; + } + } + + static bool canRemoveMetadata(String mimeType) { + switch (mimeType.toLowerCase()) { + // as of latest PixyMeta + case MimeTypes.jpeg: + case MimeTypes.tiff: + return true; + default: + return false; + } + } +} diff --git a/lib/model/apps.dart b/lib/model/apps.dart new file mode 100644 index 000000000..b7bc6e944 --- /dev/null +++ b/lib/model/apps.dart @@ -0,0 +1,78 @@ +import 'package:aves/services/common/services.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; + +final AppInventory appInventory = AppInventory._private(); + +class AppInventory { + Set<Package> _packages = {}; + List<String> _potentialAppDirs = []; + + ValueNotifier<bool> areAppNamesReadyNotifier = ValueNotifier(false); + + Iterable<Package> get _launcherPackages => _packages.where((v) => v.categoryLauncher); + + AppInventory._private(); + + Future<void> initAppNames() async { + if (_packages.isEmpty) { + debugPrint('Access installed app inventory'); + _packages = await appService.getPackages(); + _potentialAppDirs = _launcherPackages.expand((v) => v.potentialDirs).toList(); + areAppNamesReadyNotifier.value = true; + } + } + + Future<void> resetAppNames() async { + _packages.clear(); + _potentialAppDirs.clear(); + areAppNamesReadyNotifier.value = false; + } + + bool isPotentialAppDir(String dir) => _potentialAppDirs.contains(dir); + + String? getAlbumAppPackageName(String albumPath) { + final dir = pContext.split(albumPath).last; + final package = _launcherPackages.firstWhereOrNull((v) => v.potentialDirs.contains(dir)); + return package?.packageName; + } + + String? getCurrentAppName(String packageName) { + final package = _packages.firstWhereOrNull((v) => v.packageName == packageName); + return package?.currentLabel; + } +} + +class Package { + final String packageName; + final String? currentLabel, englishLabel; + final bool categoryLauncher, isSystem; + final Set<String> ownedDirs = {}; + + Package({ + required this.packageName, + required this.currentLabel, + required this.englishLabel, + required this.categoryLauncher, + required this.isSystem, + }); + + factory Package.fromMap(Map map) { + return Package( + packageName: map['packageName'] ?? '', + currentLabel: map['currentLabel'], + englishLabel: map['englishLabel'], + categoryLauncher: map['categoryLauncher'] ?? false, + isSystem: map['isSystem'] ?? false, + ); + } + + Set<String> get potentialDirs => [ + currentLabel, + englishLabel, + ...ownedDirs, + ].whereNotNull().toSet(); + + @override + String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}'; +} diff --git a/lib/model/covers.dart b/lib/model/covers.dart index 0eb13eb55..b36dee1a9 100644 --- a/lib/model/covers.dart +++ b/lib/model/covers.dart @@ -1,12 +1,14 @@ import 'dart:async'; +import 'package:aves/model/apps.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; @@ -121,7 +123,7 @@ class Covers { String? effectiveAlbumPackage(String albumPath) { final filterPackage = of(AlbumFilter(albumPath, null))?.item2; - return filterPackage ?? androidFileUtils.getAlbumAppPackageName(albumPath); + return filterPackage ?? appInventory.getAlbumAppPackageName(albumPath); } // import/export diff --git a/lib/model/device.dart b/lib/model/device.dart index 22ce40c16..2b431ac87 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -10,7 +10,7 @@ final Device device = Device._private(); class Device { late final String _userAgent; late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint; - late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto; + late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto; late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture; String get userAgent => _userAgent; @@ -25,6 +25,8 @@ class Device { bool get canRenderFlagEmojis => _canRenderFlagEmojis; + bool get canRenderSubdivisionFlagEmojis => _canRenderSubdivisionFlagEmojis; + bool get canRequestManageMedia => _canRequestManageMedia; bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper; @@ -71,6 +73,7 @@ class Device { _canPinShortcut = capabilities['canPinShortcut'] ?? false; _canPrint = capabilities['canPrint'] ?? false; _canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false; + _canRenderSubdivisionFlagEmojis = capabilities['canRenderSubdivisionFlagEmojis'] ?? false; _canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false; _canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false; _canUseCrypto = capabilities['canUseCrypto'] ?? false; diff --git a/lib/model/entry/dirs.dart b/lib/model/entry/dirs.dart index c500504a3..6db7918a4 100644 --- a/lib/model/entry/dirs.dart +++ b/lib/model/entry/dirs.dart @@ -52,7 +52,7 @@ class EntryDir { } String? _resolve() { - final vrl = VolumeRelativeDirectory.fromPath(asIs!); + final vrl = androidFileUtils.relativeDirectoryFromPath(asIs!); if (vrl == null || vrl.relativeDir.isEmpty) return asIs; var resolved = vrl.volumePath; diff --git a/lib/model/entry/entry.dart b/lib/model/entry/entry.dart index 268357e2a..13b1a5b5b 100644 --- a/lib/model/entry/entry.dart +++ b/lib/model/entry/entry.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'dart:ui'; import 'package:aves/model/entry/cache.dart'; @@ -7,13 +6,12 @@ import 'package:aves/model/entry/dirs.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/source/trash.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/format.dart'; -import 'package:aves_utils/aves_utils.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves_model/aves_model.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -80,10 +78,6 @@ class AvesEntry with AvesEntryBase { this.durationMillis = durationMillis; } - bool get canDecode => !MimeTypes.undecodableImages.contains(mimeType); - - bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType); - AvesEntry copyWith({ int? id, String? uri, @@ -225,15 +219,6 @@ class AvesEntry with AvesEntryBase { return _extension; } - String? get storagePath => trashed ? trashDetails?.path : path; - - String? get storageDirectory => trashed ? pContext.dirname(trashDetails!.path) : directory; - - bool get isMissingAtPath { - final _storagePath = storagePath; - return _storagePath != null && !File(_storagePath).existsSync(); - } - // the MIME type reported by the Media Store is unreliable // so we use the one found during cataloguing if possible String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType; @@ -323,18 +308,6 @@ class AvesEntry with AvesEntryBase { 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; diff --git a/lib/model/entry/extensions/images.dart b/lib/model/entry/extensions/images.dart index 46e18a337..ee9dbafd1 100644 --- a/lib/model/entry/extensions/images.dart +++ b/lib/model/entry/extensions/images.dart @@ -7,7 +7,7 @@ import 'package:aves/model/entry/cache.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/painting.dart'; extension ExtraAvesEntryImages on AvesEntry { bool isThumbnailReady({double extent = 0}) => _isReady(_getThumbnailProviderKey(extent)); diff --git a/lib/model/entry/extensions/info.dart b/lib/model/entry/extensions/info.dart index 46649a95f..04b064a1d 100644 --- a/lib/model/entry/extensions/info.dart +++ b/lib/model/entry/extensions/info.dart @@ -9,11 +9,11 @@ import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart'; import 'package:aves/theme/colors.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; extension ExtraAvesEntryInfo on AvesEntry { @@ -115,7 +115,7 @@ extension ExtraAvesEntryInfo on AvesEntry { final dirName = [ 'Stream ${index.toString().padLeft(indexDigits, '0')}', typeText, - ].join(Constants.separator); + ].join(AText.separator); final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream); if (formattedStreamTags.isNotEmpty) { final color = colors.fromString(typeText); diff --git a/lib/model/entry/extensions/location.dart b/lib/model/entry/extensions/location.dart index 97f477c15..e33c4ab72 100644 --- a/lib/model/entry/extensions/location.dart +++ b/lib/model/entry/extensions/location.dart @@ -12,6 +12,8 @@ import 'package:flutter/foundation.dart'; import 'package:latlong2/latlong.dart'; extension ExtraAvesEntryLocation on AvesEntry { + static final _invalidLocalityPattern = RegExp(r'^[-+\dA-Z]+$'); + LatLng? get latLng => hasGps ? LatLng(catalogMetadata!.latitude!, catalogMetadata!.longitude!) : null; Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async { @@ -53,18 +55,17 @@ extension ExtraAvesEntryLocation on AvesEntry { ) : call()); if (addresses.isNotEmpty) { - final address = addresses.first; - final cc = address.countryCode?.toUpperCase(); - final cn = address.countryName; - final aa = address.adminArea; + final v = addresses.first; + var locality = v.locality ?? v.subLocality ?? v.featureName; + if (locality == null || _invalidLocalityPattern.hasMatch(locality) || {v.subThoroughfare, v.countryName}.contains(locality)) { + locality = v.subAdminArea; + } addressDetails = AddressDetails( id: id, - countryCode: cc, - countryName: cn, - adminArea: aa, - // if country & admin fields are null, it is likely the ocean, - // which is identified by `featureName` but we default to the address line anyway - locality: address.locality ?? (cc == null && cn == null && aa == null ? address.addressLine : null), + countryCode: v.countryCode?.toUpperCase(), + countryName: v.countryName, + adminArea: v.adminArea, + locality: locality, ); } } catch (error, stack) { diff --git a/lib/model/entry/extensions/metadata_edition.dart b/lib/model/entry/extensions/metadata_edition.dart index eaa156585..9d1f2f7c3 100644 --- a/lib/model/entry/extensions/metadata_edition.dart +++ b/lib/model/entry/extensions/metadata_edition.dart @@ -1,20 +1,20 @@ import 'dart:convert'; import 'dart:io'; +import 'package:aves/convert/convert.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/catalog.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/metadata/date_modifier.dart'; -import 'package:aves/model/metadata/enums/date_field_source.dart'; -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/fields.dart'; -import 'package:aves/ref/exif.dart'; -import 'package:aves/ref/iptc.dart'; +import 'package:aves/ref/metadata/exif.dart'; +import 'package:aves/ref/metadata/iptc.dart'; import 'package:aves/ref/mime_types.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/xmp.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; @@ -27,7 +27,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { final appliedModifier = await _applyDateModifierToEntry(userModifier); if (appliedModifier == null) { - if (!isMissingAtPath && userModifier.action != DateEditAction.copyField) { + if (isValid && userModifier.action != DateEditAction.copyField) { await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null); } return {}; @@ -54,7 +54,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { editCreateDateXmp(descriptions, appliedModifier.setDateTime); break; case DateEditAction.shift: - final xmpDate = XMP.getString(descriptions, XMP.xmpCreateDate, namespace: Namespaces.xmp); + final xmpDate = XMP.getString(descriptions, XmpAttributes.xmpCreateDate, namespace: XmpNamespaces.xmp); if (xmpDate != null) { final date = DateTime.tryParse(xmpDate); if (date != null) { @@ -262,18 +262,18 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { if (editTitle) { modified |= XMP.setAttribute( descriptions, - XMP.dcTitle, + XmpElements.dcTitle, title, - namespace: Namespaces.dc, + namespace: XmpNamespaces.dc, strat: XmpEditStrategy.always, ); } if (editDescription) { modified |= XMP.setAttribute( descriptions, - XMP.dcDescription, + XmpElements.dcDescription, description, - namespace: Namespaces.dc, + namespace: XmpNamespaces.dc, strat: XmpEditStrategy.always, ); } @@ -417,9 +417,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { static bool editCreateDateXmp(List<XmlNode> descriptions, DateTime? date) { return XMP.setAttribute( descriptions, - XMP.xmpCreateDate, + XmpAttributes.xmpCreateDate, date != null ? XMP.toXmpDate(date) : null, - namespace: Namespaces.xmp, + namespace: XmpNamespaces.xmp, strat: XmpEditStrategy.always, ); } @@ -428,9 +428,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { static bool editTagsXmp(List<XmlNode> descriptions, Set<String> tags) { return XMP.setStringBag( descriptions, - XMP.dcSubject, + XmpElements.dcSubject, tags, - namespace: Namespaces.dc, + namespace: XmpNamespaces.dc, strat: XmpEditStrategy.always, ); } @@ -441,17 +441,17 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { modified |= XMP.setAttribute( descriptions, - XMP.xmpRating, + XmpElements.xmpRating, (rating ?? 0) == 0 ? null : '$rating', - namespace: Namespaces.xmp, + namespace: XmpNamespaces.xmp, strat: XmpEditStrategy.always, ); modified |= XMP.setAttribute( descriptions, - XMP.msPhotoRating, + XmpElements.msPhotoRating, XMP.toMsPhotoRating(rating), - namespace: Namespaces.microsoftPhoto, + namespace: XmpNamespaces.microsoftPhoto, strat: XmpEditStrategy.updateIfPresent, ); @@ -464,23 +464,23 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { modified |= XMP.removeElements( descriptions, - XMP.containerDirectory, - Namespaces.gContainer, + XmpElements.containerDirectory, + XmpNamespaces.gContainer, ); modified |= [ - XMP.gCameraMicroVideo, - XMP.gCameraMicroVideoVersion, - XMP.gCameraMicroVideoOffset, - XMP.gCameraMicroVideoPresentationTimestampUs, - XMP.gCameraMotionPhoto, - XMP.gCameraMotionPhotoVersion, - XMP.gCameraMotionPhotoPresentationTimestampUs, + XmpAttributes.gCameraMicroVideo, + XmpAttributes.gCameraMicroVideoVersion, + XmpAttributes.gCameraMicroVideoOffset, + XmpAttributes.gCameraMicroVideoPresentationTimestampUs, + XmpAttributes.gCameraMotionPhoto, + XmpAttributes.gCameraMotionPhotoVersion, + XmpAttributes.gCameraMotionPhotoPresentationTimestampUs, ].fold<bool>(modified, (prev, name) { return prev |= XMP.removeElements( descriptions, name, - Namespaces.gCamera, + XmpNamespaces.gCamera, ); }); diff --git a/lib/model/entry/extensions/multipage.dart b/lib/model/entry/extensions/multipage.dart index f77f252b1..977dd1733 100644 --- a/lib/model/entry/extensions/multipage.dart +++ b/lib/model/entry/extensions/multipage.dart @@ -7,8 +7,6 @@ import 'package:aves/services/common/services.dart'; import 'package:collection/collection.dart'; extension ExtraAvesEntryMultipage on AvesEntry { - static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$'); - bool get isMultiPage => (catalogMetadata?.isMultiPage ?? false) || isBurst; bool get isBurst => burstEntries?.isNotEmpty == true; @@ -18,11 +16,13 @@ extension ExtraAvesEntryMultipage on AvesEntry { bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy; - String? get burstKey { + String? getBurstKey(List<String> patterns) { if (filenameWithoutExtension != null) { - final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!); - if (match != null) { - return '$directory/${match.group(1)}'; + for (final pattern in patterns) { + final match = RegExp(pattern).firstMatch(filenameWithoutExtension!); + if (match != null) { + return '$directory/${match.group(1)}'; + } } } return null; diff --git a/lib/model/entry/extensions/props.dart b/lib/model/entry/extensions/props.dart index ed1c3ef1c..f5d064c2f 100644 --- a/lib/model/entry/extensions/props.dart +++ b/lib/model/entry/extensions/props.dart @@ -1,63 +1,113 @@ +import 'dart:io'; import 'dart:ui'; +import 'package:aves/model/app/support.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/trash.dart'; import 'package:aves/ref/mime_types.dart'; +import 'package:aves/ref/unicode.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/utils/android_file_utils.dart'; extension ExtraAvesEntryProps on AvesEntry { + bool get isValid => !isMissingAtPath && sizeBytes != 0 && width > 0 && height > 0; + + // type + String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*'); + bool get canHaveAlpha => MimeTypes.canHaveAlpha(mimeType); + bool get isSvg => mimeType == MimeTypes.svg; - // guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels) - bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw; - - // Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported" - // but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below, - // and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested. - bool get _supportedByBitmapRegionDecoder => - [ - MimeTypes.heic, - MimeTypes.heif, - MimeTypes.jpeg, - MimeTypes.png, - MimeTypes.webp, - MimeTypes.arw, - MimeTypes.cr2, - MimeTypes.nef, - MimeTypes.nrw, - MimeTypes.orf, - MimeTypes.pef, - MimeTypes.raf, - MimeTypes.rw2, - MimeTypes.srw, - ].contains(mimeType) && - !isAnimated; - - bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; - - bool get useTiles => supportTiling && (width > 4096 || height > 4096); - - bool get isRaw => MimeTypes.rawImages.contains(mimeType); + bool get isRaw => MimeTypes.isRaw(mimeType); bool get isImage => MimeTypes.isImage(mimeType); bool get isVideo => MimeTypes.isVideo(mimeType); + // size + + bool get useTiles => canDecodeRegion && (width > 4096 || height > 4096); + + bool get isSized => width > 0 && height > 0; + + Size videoDisplaySize(double sar) { + final size = displaySize; + if (sar != 1) { + final dar = displayAspectRatio * sar; + final w = size.width; + final h = size.height; + if (w >= h) return Size(w, w / dar); + if (h > w) return Size(h * dar, h); + } + return size; + } + + // text + + String get resolutionText { + final ws = width; + final hs = height; + return isRotated ? '$hs${AText.resolutionSeparator}$ws' : '$ws${AText.resolutionSeparator}$hs'; + } + + String get aspectRatioText { + const separator = UniChars.ratio; + if (width > 0 && height > 0) { + final gcd = width.gcd(height); + final w = width ~/ gcd; + final h = height ~/ gcd; + return isRotated ? '$h$separator$w' : '$w$separator$h'; + } else { + return '?$separator?'; + } + } + + // catalog + bool get isAnimated => catalogMetadata?.isAnimated ?? false; bool get isGeotiff => catalogMetadata?.isGeotiff ?? false; bool get is360 => catalogMetadata?.is360 ?? false; - bool get isMediaStoreContent => uri.startsWith('content://media/'); + // trash - bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains); + bool get isExpiredTrash { + final dateMillis = trashDetails?.dateMillis; + if (dateMillis == null) return false; + return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).isBefore(DateTime.now()); + } - bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false; + int? get trashDaysLeft { + final dateMillis = trashDetails?.dateMillis; + if (dateMillis == null) return null; + return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays; + } - bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent); + // storage + + String? get storageDirectory => trashed ? pContext.dirname(trashDetails!.path) : directory; + + bool get isMissingAtPath { + final _storagePath = trashed ? trashDetails?.path : path; + return _storagePath != null && !File(_storagePath).existsSync(); + } + + // providers + + bool get _isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false; + + bool get _isMediaStoreContent => uri.startsWith(AndroidFileUtils.mediaStoreUriRoot); + + bool get isMediaStoreMediaContent => _isMediaStoreContent && AndroidFileUtils.mediaUriPathRoots.any(uri.contains); + + // edition + + bool get canEdit => !settings.isReadOnly && path != null && !trashed && (_isMediaStoreContent || _isVaultContent); bool get canEditDate => canEdit && (canEditExif || canEditXmp); @@ -73,47 +123,17 @@ extension ExtraAvesEntryProps on AvesEntry { bool get canFlip => canEdit && canEditExif; - bool get canEditExif => MimeTypes.canEditExif(mimeType); + // app support - bool get canEditIptc => MimeTypes.canEditIptc(mimeType); + bool get canDecode => AppSupport.canDecode(mimeType); - bool get canEditXmp => MimeTypes.canEditXmp(mimeType); + bool get canDecodeRegion => AppSupport.canDecodeRegion(mimeType) && !isAnimated; - bool get canRemoveMetadata => MimeTypes.canRemoveMetadata(mimeType); + bool get canEditExif => AppSupport.canEditExif(mimeType); - static const ratioSeparator = '\u2236'; - static const resolutionSeparator = ' \u00D7 '; + bool get canEditIptc => AppSupport.canEditIptc(mimeType); - bool get isSized => width > 0 && height > 0; + bool get canEditXmp => AppSupport.canEditXmp(mimeType); - String get resolutionText { - final ws = width; - final hs = height; - return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs'; - } - - String get aspectRatioText { - if (width > 0 && height > 0) { - final gcd = width.gcd(height); - final w = width ~/ gcd; - final h = height ~/ gcd; - return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h'; - } else { - return '?$ratioSeparator?'; - } - } - - Size videoDisplaySize(double sar) { - final size = displaySize; - if (sar != 1) { - final dar = displayAspectRatio * sar; - final w = size.width; - final h = size.height; - if (w >= h) return Size(w, w / dar); - if (h > w) return Size(h * dar, h); - } - return size; - } - - int get megaPixels => (width * height / 1000000).round(); + bool get canRemoveMetadata => AppSupport.canRemoveMetadata(mimeType); } diff --git a/lib/model/actions/events.dart b/lib/model/events.dart similarity index 100% rename from lib/model/actions/events.dart rename to lib/model/events.dart diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart index 8c5eeea57..5c56b8ad3 100644 --- a/lib/model/favourites.dart +++ b/lib/model/favourites.dart @@ -2,9 +2,10 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; final Favourites favourites = Favourites._private(); diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 226436bbf..7f5f8aa77 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -3,8 +3,8 @@ 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'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index 2c822fecf..4a17edb2e 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -2,13 +2,11 @@ import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/filters/filters.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/widgets/aves_app.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves_map/aves_map.dart'; -import 'package:flutter/material.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -61,7 +59,7 @@ class CoordinateFilter extends CollectionFilter { bool get exclusiveProp => false; @override - String get universalLabel => _formatBounds(lookupAppLocalizations(AvesApp.supportedLocales.first), CoordinateFormat.decimal); + String get universalLabel => _formatBounds(lookupAppLocalizations(AppLocalizations.supportedLocales.first), CoordinateFormat.decimal); @override String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read<Settings>().coordinateFormat); diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index 413da4c86..3b5ff0333 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -5,7 +5,7 @@ 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/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class FavouriteFilter extends CollectionFilter { diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index aab90081e..0c547defd 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -1,6 +1,7 @@ import 'package:aves/model/device.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/emoji_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; @@ -11,23 +12,31 @@ class LocationFilter extends CoveredCollectionFilter { final LocationLevel level; late final String _location; - late final String? _countryCode; + late final String? _code; late final EntryFilter _test; @override - List<Object?> get props => [level, _location, _countryCode, reversed]; + List<Object?> get props => [level, _location, _code, reversed]; LocationFilter(this.level, String location, {super.reversed = false}) { final split = location.split(locationSeparator); _location = split.isNotEmpty ? split[0] : location; - _countryCode = split.length > 1 ? split[1] : null; + _code = split.length > 1 ? split[1] : null; if (_location.isEmpty) { _test = (entry) => !entry.hasGps; - } else if (level == LocationLevel.country) { - _test = (entry) => entry.addressDetails?.countryCode == _countryCode; - } else if (level == LocationLevel.place) { - _test = (entry) => entry.addressDetails?.place == _location; + } else { + switch (level) { + case LocationLevel.country: + _test = (entry) => entry.addressDetails?.countryCode == _code; + break; + case LocationLevel.state: + _test = (entry) => entry.addressDetails?.stateCode == _code; + break; + case LocationLevel.place: + _test = (entry) => entry.addressDetails?.place == _location; + break; + } } } @@ -40,16 +49,29 @@ class LocationFilter extends CoveredCollectionFilter { } @override - Map<String, dynamic> toMap() => { - 'type': type, - 'level': level.toString(), - 'location': _countryCode != null ? countryNameAndCode : _location, - 'reversed': reversed, - }; + Map<String, dynamic> toMap() { + String location = _location; + switch (level) { + case LocationLevel.country: + case LocationLevel.state: + if (_code != null) { + location = _nameAndCode; + } + break; + case LocationLevel.place: + break; + } + return { + 'type': type, + 'level': level.toString(), + 'location': location, + 'reversed': reversed, + }; + } - String get countryNameAndCode => '$_location$locationSeparator$_countryCode'; + String get _nameAndCode => '$_location$locationSeparator$_code'; - String? get countryCode => _countryCode; + String? get code => _code; String get place => _location; @@ -71,11 +93,9 @@ class LocationFilter extends CoveredCollectionFilter { return Icon(AIcons.locationUnlocated, size: size); } switch (level) { - case LocationLevel.place: - return Icon(AIcons.place, size: size); case LocationLevel.country: - if (_countryCode != null && device.canRenderFlagEmojis) { - final flag = countryCodeToFlag(_countryCode); + if (_code != null && device.canRenderFlagEmojis) { + final flag = EmojiUtils.countryCodeToFlag(_code); if (flag != null) { return Text( flag, @@ -85,6 +105,20 @@ class LocationFilter extends CoveredCollectionFilter { } } return Icon(AIcons.country, size: size); + case LocationLevel.state: + if (_code != null && device.canRenderSubdivisionFlagEmojis) { + final flag = EmojiUtils.stateCodeToFlag(_code); + if (flag != null) { + return Text( + flag, + style: TextStyle(fontSize: size), + textScaleFactor: 1.0, + ); + } + } + return Icon(AIcons.state, size: size); + case LocationLevel.place: + return Icon(AIcons.place, size: size); } } @@ -92,16 +126,7 @@ class LocationFilter extends CoveredCollectionFilter { String get category => type; @override - String get key => '$type-$reversed-$level-$_location'; - - // U+0041 Latin Capital letter A - // U+1F1E6 🇦 REGIONAL INDICATOR SYMBOL LETTER A - static const _countryCodeToFlagDiff = 0x1F1E6 - 0x0041; - - static String? countryCodeToFlag(String? code) { - if (code == null || code.length != 2) return null; - return String.fromCharCodes(code.toUpperCase().codeUnits.map((letter) => letter += _countryCodeToFlagDiff)); - } + String get key => '$type-$reversed-$level-$code-$place'; } -enum LocationLevel { place, country } +enum LocationLevel { place, state, country } diff --git a/lib/model/filters/placeholder.dart b/lib/model/filters/placeholder.dart index d2438e762..32f22b1f4 100644 --- a/lib/model/filters/placeholder.dart +++ b/lib/model/filters/placeholder.dart @@ -11,12 +11,14 @@ class PlaceholderFilter extends CollectionFilter { static const type = 'placeholder'; static const _country = 'country'; + static const _state = 'state'; static const _place = 'place'; final String placeholder; late final IconData _icon; static final country = PlaceholderFilter._private(_country); + static final state = PlaceholderFilter._private(_state); static final place = PlaceholderFilter._private(_place); @override @@ -27,6 +29,9 @@ class PlaceholderFilter extends CollectionFilter { case _country: _icon = AIcons.country; break; + case _state: + _icon = AIcons.state; + break; case _place: _icon = AIcons.place; break; @@ -48,6 +53,7 @@ class PlaceholderFilter extends CollectionFilter { Future<String?> toTag(AvesEntry entry) async { switch (placeholder) { case _country: + case _state: case _place: if (!entry.isCatalogued) { await entry.catalog(background: false, force: false, persist: true); @@ -60,8 +66,14 @@ class PlaceholderFilter extends CollectionFilter { final address = entry.addressDetails; if (address == null) return null; - if (placeholder == _country) return address.countryName; - if (placeholder == _place) return address.place; + switch (placeholder) { + case _country: + return address.countryName; + case _state: + return address.stateName; + case _place: + return address.place; + } break; } return null; @@ -81,6 +93,8 @@ class PlaceholderFilter extends CollectionFilter { switch (placeholder) { case _country: return context.l10n.tagPlaceholderCountry; + case _state: + return context.l10n.tagPlaceholderState; case _place: return context.l10n.tagPlaceholderPlace; default: diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index 9df1f55f6..523a1b156 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -4,14 +4,13 @@ import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class QueryFilter extends CollectionFilter { static const type = 'query'; - static final RegExp exactRegex = RegExp('^"(.*)"\$'); + static final exactRegex = RegExp('^"(.*)"\$'); final String query; final bool colorful, live; diff --git a/lib/model/filters/rating.dart b/lib/model/filters/rating.dart index 0cf9cfbc3..631d13765 100644 --- a/lib/model/filters/rating.dart +++ b/lib/model/filters/rating.dart @@ -1,4 +1,5 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/ref/unicode.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; @@ -67,7 +68,7 @@ class RatingFilter extends CollectionFilter { case 0: return context.l10n.filterNoRatingLabel; default: - return '\u2B50' * rating; + return UniChars.whiteMediumStar * rating; } } } diff --git a/lib/model/filters/recent.dart b/lib/model/filters/recent.dart index d8cfb3e78..a30e6cfbc 100644 --- a/lib/model/filters/recent.dart +++ b/lib/model/filters/recent.dart @@ -1,7 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class RecentlyAddedFilter extends CollectionFilter { static const type = 'recently_added'; diff --git a/lib/model/filters/trash.dart b/lib/model/filters/trash.dart index c995f95b2..3957254b7 100644 --- a/lib/model/filters/trash.dart +++ b/lib/model/filters/trash.dart @@ -2,7 +2,7 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class TrashFilter extends CollectionFilter { static const type = 'trash'; diff --git a/lib/model/geotiff.dart b/lib/model/geotiff.dart index a557039ae..efa6afb46 100644 --- a/lib/model/geotiff.dart +++ b/lib/model/geotiff.dart @@ -4,11 +4,12 @@ import 'dart:ui' as ui; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/images.dart'; -import 'package:aves/ref/geotiff.dart'; +import 'package:aves/ref/metadata/geotiff.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves_map/aves_map.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; import 'package:latlong2/latlong.dart'; import 'package:proj4dart/proj4dart.dart' as proj4; @@ -49,7 +50,7 @@ class MappedGeoTiff with MapOverlay { static final tileImagePaint = Paint(); static final tileMissingPaint = Paint() ..style = PaintingStyle.fill - ..color = Colors.black; + ..color = const Color(0xFF000000); MappedGeoTiff({ required GeoTiffInfo info, diff --git a/lib/model/highlight.dart b/lib/model/highlight.dart index 642abe8ac..7a9c94de0 100644 --- a/lib/model/highlight.dart +++ b/lib/model/highlight.dart @@ -1,6 +1,6 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/painting.dart'; class HighlightInfo extends ChangeNotifier { final EventBus eventBus = EventBus(); diff --git a/lib/model/metadata/address.dart b/lib/model/metadata/address.dart index b05ecd988..2680799ec 100644 --- a/lib/model/metadata/address.dart +++ b/lib/model/metadata/address.dart @@ -1,17 +1,21 @@ +import 'package:aves/geo/states.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; @immutable class AddressDetails extends Equatable { final int id; final String? countryCode, countryName, adminArea, locality; - String? get place => locality != null && locality!.isNotEmpty ? locality : adminArea; - @override List<Object?> get props => [id, countryCode, countryName, adminArea, locality]; + String? get place => locality != null && locality!.isNotEmpty ? locality : adminArea; + + String? get stateCode => GeoStates.stateCodeByName[stateName]; + + String? get stateName => GeoStates.stateCountryCodes.contains(countryCode) ? adminArea : null; + const AddressDetails({ required this.id, this.countryCode, diff --git a/lib/model/metadata/date_modifier.dart b/lib/model/metadata/date_modifier.dart index 7f373ac50..4fd675122 100644 --- a/lib/model/metadata/date_modifier.dart +++ b/lib/model/metadata/date_modifier.dart @@ -1,8 +1,6 @@ -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/fields.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; @immutable class DateModifier extends Equatable { diff --git a/lib/model/metadata/enums/date_field_source.dart b/lib/model/metadata/enums/date_field_source.dart deleted file mode 100644 index 3f3d12a25..000000000 --- a/lib/model/metadata/enums/date_field_source.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/fields.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/widgets.dart'; - -extension ExtraDateFieldSource on DateFieldSource { - String getText(BuildContext context) { - switch (this) { - case DateFieldSource.fileModifiedDate: - return context.l10n.editEntryDateDialogSourceFileModifiedDate; - case DateFieldSource.exifDate: - return 'Exif date'; - case DateFieldSource.exifDateOriginal: - return 'Exif original date'; - case DateFieldSource.exifDateDigitized: - return 'Exif digitized date'; - case DateFieldSource.exifGpsDate: - return 'Exif GPS date'; - } - } - - MetadataField? toMetadataField() { - switch (this) { - case DateFieldSource.fileModifiedDate: - return null; - case DateFieldSource.exifDate: - return MetadataField.exifDate; - case DateFieldSource.exifDateOriginal: - return MetadataField.exifDateOriginal; - case DateFieldSource.exifDateDigitized: - return MetadataField.exifDateDigitized; - case DateFieldSource.exifGpsDate: - return MetadataField.exifGpsDatestamp; - } - } -} diff --git a/lib/model/metadata/enums/metadata_type.dart b/lib/model/metadata/enums/metadata_type.dart deleted file mode 100644 index e7481d5ae..000000000 --- a/lib/model/metadata/enums/metadata_type.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:aves/model/metadata/enums/enums.dart'; - -class MetadataTypes { - static const main = { - MetadataType.exif, - MetadataType.xmp, - }; - - static const common = { - MetadataType.exif, - MetadataType.xmp, - MetadataType.comment, - MetadataType.iccProfile, - MetadataType.iptc, - MetadataType.photoshopIrb, - }; - - static const jpeg = { - MetadataType.jfif, - MetadataType.jpegAdobe, - MetadataType.jpegDucky, - }; -} - -extension ExtraMetadataType on MetadataType { - // match `metadata-extractor` directory names - String getText() { - switch (this) { - case MetadataType.comment: - return 'Comment'; - case MetadataType.exif: - return 'Exif'; - case MetadataType.iccProfile: - return 'ICC Profile'; - case MetadataType.iptc: - return 'IPTC'; - case MetadataType.jfif: - return 'JFIF'; - case MetadataType.jpegAdobe: - return 'Adobe JPEG'; - case MetadataType.jpegDucky: - return 'Ducky'; - case MetadataType.mp4: - return 'MP4'; - case MetadataType.photoshopIrb: - return 'Photoshop'; - case MetadataType.xmp: - return 'XMP'; - } - } - - String get toPlatform { - switch (this) { - case MetadataType.comment: - return 'comment'; - case MetadataType.exif: - return 'exif'; - case MetadataType.iccProfile: - return 'icc_profile'; - case MetadataType.iptc: - return 'iptc'; - case MetadataType.jfif: - return 'jfif'; - case MetadataType.jpegAdobe: - return 'jpeg_adobe'; - case MetadataType.jpegDucky: - return 'jpeg_ducky'; - case MetadataType.mp4: - return 'mp4'; - case MetadataType.photoshopIrb: - return 'photoshop_irb'; - case MetadataType.xmp: - return 'xmp'; - } - } -} diff --git a/lib/model/naming_pattern.dart b/lib/model/naming_pattern.dart index e24e5221f..2fd502291 100644 --- a/lib/model/naming_pattern.dart +++ b/lib/model/naming_pattern.dart @@ -1,6 +1,6 @@ import 'package:aves/model/entry/entry.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; @immutable diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 212e8a424..eeb911869 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -1,14 +1,13 @@ -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/actions/entry_set_actions.dart'; +import 'dart:ui'; + import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/naming_pattern.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/ref/mime_types.dart'; +import 'package:aves/utils/colors.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; -import 'package:flutter/material.dart'; +import 'package:aves_model/aves_model.dart'; class SettingsDefaults { // app @@ -64,10 +63,7 @@ class SettingsDefaults { // filter grids static const albumGroupFactor = AlbumChipGroupFactor.importance; - static const albumSortFactor = ChipSortFactor.name; - static const countrySortFactor = ChipSortFactor.name; - static const placeSortFactor = ChipSortFactor.name; - static const tagSortFactor = ChipSortFactor.name; + static const chipListSortFactor = ChipSortFactor.name; // viewer static const viewerQuickActions = [ @@ -104,8 +100,8 @@ class SettingsDefaults { static const subtitleTextAlignment = TextAlign.center; static const subtitleTextPosition = SubtitlePosition.bottom; static const subtitleShowOutline = true; - static const subtitleTextColor = Colors.white; - static const subtitleBackgroundColor = Colors.transparent; + static const subtitleTextColor = Color(0xFFFFFFFF); + static const subtitleBackgroundColor = ColorUtils.transparentBlack; // info static const infoMapZoom = 12.0; diff --git a/lib/model/settings/enums/accessibility_animations.dart b/lib/model/settings/enums/accessibility_animations.dart index e9b6b1184..a336c2601 100644 --- a/lib/model/settings/enums/accessibility_animations.dart +++ b/lib/model/settings/enums/accessibility_animations.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves_model/aves_model.dart'; extension ExtraAccessibilityAnimations on AccessibilityAnimations { bool get animate { diff --git a/lib/model/settings/enums/accessibility_timeout.dart b/lib/model/settings/enums/accessibility_timeout.dart new file mode 100644 index 000000000..6c274a652 --- /dev/null +++ b/lib/model/settings/enums/accessibility_timeout.dart @@ -0,0 +1,26 @@ +import 'package:aves/services/accessibility_service.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves_model/aves_model.dart'; + +extension ExtraAccessibilityTimeout on AccessibilityTimeout { + Future<Duration> getSnackBarDuration(bool hasAction) async { + switch (this) { + case AccessibilityTimeout.system: + if (hasAction) { + return Duration(milliseconds: await (AccessibilityService.getRecommendedTimeToTakeAction(Durations.opToastActionDisplay))); + } else { + return Duration(milliseconds: await (AccessibilityService.getRecommendedTimeToRead(Durations.opToastTextDisplay))); + } + case AccessibilityTimeout.s1: + return const Duration(seconds: 1); + case AccessibilityTimeout.s3: + return const Duration(seconds: 3); + case AccessibilityTimeout.s5: + return const Duration(seconds: 5); + case AccessibilityTimeout.s10: + return const Duration(seconds: 10); + case AccessibilityTimeout.s30: + return const Duration(seconds: 30); + } + } +} diff --git a/lib/model/settings/enums/coordinate_format.dart b/lib/model/settings/enums/coordinate_format.dart index c03fe3727..ed81bad64 100644 --- a/lib/model/settings/enums/coordinate_format.dart +++ b/lib/model/settings/enums/coordinate_format.dart @@ -1,5 +1,5 @@ import 'package:aves/l10n/l10n.dart'; -import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/model/settings/enums/display_refresh_rate_mode.dart b/lib/model/settings/enums/display_refresh_rate_mode.dart index 79bf93508..942d901b9 100644 --- a/lib/model/settings/enums/display_refresh_rate_mode.dart +++ b/lib/model/settings/enums/display_refresh_rate_mode.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; diff --git a/lib/model/settings/enums/entry_background.dart b/lib/model/settings/enums/entry_background.dart index b4fc3a636..9661c4d7d 100644 --- a/lib/model/settings/enums/entry_background.dart +++ b/lib/model/settings/enums/entry_background.dart @@ -1,5 +1,6 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:flutter/material.dart'; +import 'dart:ui'; + +import 'package:aves_model/aves_model.dart'; extension ExtraEntryBackground on EntryBackground { bool get isColor { @@ -15,10 +16,10 @@ extension ExtraEntryBackground on EntryBackground { Color get color { switch (this) { case EntryBackground.white: - return Colors.white; + return const Color(0xFFFFFFFF); case EntryBackground.black: default: - return Colors.black; + return const Color(0xFF000000); } } } diff --git a/lib/model/settings/enums/home_page.dart b/lib/model/settings/enums/home_page.dart index 4cdf8b057..5adb6788b 100644 --- a/lib/model/settings/enums/home_page.dart +++ b/lib/model/settings/enums/home_page.dart @@ -1,6 +1,6 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves_model/aves_model.dart'; extension ExtraHomePageSetting on HomePageSetting { String get routeName { diff --git a/lib/model/settings/enums/screen_on.dart b/lib/model/settings/enums/screen_on.dart index a2f168c64..2d6e63bfc 100644 --- a/lib/model/settings/enums/screen_on.dart +++ b/lib/model/settings/enums/screen_on.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves_model/aves_model.dart'; extension ExtraKeepScreenOn on KeepScreenOn { void apply() { diff --git a/lib/model/settings/enums/subtitle_position.dart b/lib/model/settings/enums/subtitle_position.dart index 988545095..a37ad7254 100644 --- a/lib/model/settings/enums/subtitle_position.dart +++ b/lib/model/settings/enums/subtitle_position.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:flutter/widgets.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/painting.dart'; extension ExtraSubtitlePosition on SubtitlePosition { TextAlignVertical toTextAlignVertical() { diff --git a/lib/model/settings/enums/theme_brightness.dart b/lib/model/settings/enums/theme_brightness.dart index af38e11c6..5bcbd7576 100644 --- a/lib/model/settings/enums/theme_brightness.dart +++ b/lib/model/settings/enums/theme_brightness.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; extension ExtraAvesThemeBrightness on AvesThemeBrightness { diff --git a/lib/model/settings/enums/thumbnail_overlay_location_icon.dart b/lib/model/settings/enums/thumbnail_overlay_location_icon.dart deleted file mode 100644 index c241fe853..000000000 --- a/lib/model/settings/enums/thumbnail_overlay_location_icon.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:flutter/widgets.dart'; - -extension ExtraThumbnailOverlayLocationIcon on ThumbnailOverlayLocationIcon { - IconData getIcon(BuildContext context) { - switch (this) { - case ThumbnailOverlayLocationIcon.unlocated: - return AIcons.locationUnlocated; - case ThumbnailOverlayLocationIcon.located: - case ThumbnailOverlayLocationIcon.none: - return AIcons.location; - } - } -} diff --git a/lib/model/settings/enums/thumbnail_overlay_tag_icon.dart b/lib/model/settings/enums/thumbnail_overlay_tag_icon.dart deleted file mode 100644 index 969900e82..000000000 --- a/lib/model/settings/enums/thumbnail_overlay_tag_icon.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:flutter/widgets.dart'; - -extension ExtraThumbnailOverlayTagIcon on ThumbnailOverlayTagIcon { - IconData getIcon(BuildContext context) { - switch (this) { - case ThumbnailOverlayTagIcon.tagged: - return AIcons.tag; - case ThumbnailOverlayTagIcon.untagged: - return AIcons.tagUntagged; - case ThumbnailOverlayTagIcon.none: - return AIcons.tag; - } - } -} diff --git a/lib/model/settings/enums/video_loop_mode.dart b/lib/model/settings/enums/video_loop_mode.dart index 0799fcfe8..587f5f815 100644 --- a/lib/model/settings/enums/video_loop_mode.dart +++ b/lib/model/settings/enums/video_loop_mode.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves_model/aves_model.dart'; extension ExtraVideoLoopMode on VideoLoopMode { static const shortVideoThreshold = Duration(seconds: 30); diff --git a/lib/model/settings/enums/viewer_transition.dart b/lib/model/settings/enums/viewer_transition.dart index 1f4ff4352..b97b5b77c 100644 --- a/lib/model/settings/enums/viewer_transition.dart +++ b/lib/model/settings/enums/viewer_transition.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/viewer/controls/controller.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; extension ExtraViewerTransition on ViewerTransition { diff --git a/lib/model/settings/enums/widget_shape.dart b/lib/model/settings/enums/widget_shape.dart index deeed752a..adb2d6730 100644 --- a/lib/model/settings/enums/widget_shape.dart +++ b/lib/model/settings/enums/widget_shape.dart @@ -1,6 +1,6 @@ import 'package:aves/model/entry/entry.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:flutter/widgets.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/painting.dart'; extension ExtraWidgetShape on WidgetShape { Path path(Size widgetSize, double devicePixelRatio) { diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index ea63e38e3..755ca9dbc 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -3,18 +3,14 @@ import 'dart:convert'; import 'dart:math'; import 'package:aves/app_flavor.dart'; -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/device.dart'; 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/settings/defaults.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/map_style.dart'; -import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/ref/bursts.dart'; import 'package:aves/services/accessibility_service.dart'; -import 'package:aves_utils/aves_utils.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/search/page.dart'; @@ -23,9 +19,12 @@ import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/places_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:latlong2/latlong.dart'; final Settings settings = Settings._private(); @@ -90,6 +89,7 @@ class Settings extends ChangeNotifier { static const drawerPageBookmarksKey = 'drawer_page_bookmarks'; // collection + static const collectionBurstPatternsKey = 'collection_burst_patterns'; static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionSortReverseKey = 'collection_sort_reverse'; @@ -107,10 +107,12 @@ class Settings extends ChangeNotifier { static const albumGroupFactorKey = 'album_group_factor'; static const albumSortFactorKey = 'album_sort_factor'; static const countrySortFactorKey = 'country_sort_factor'; + static const stateSortFactorKey = 'state_sort_factor'; static const placeSortFactorKey = 'place_sort_factor'; static const tagSortFactorKey = 'tag_sort_factor'; static const albumSortReverseKey = 'album_sort_reverse'; static const countrySortReverseKey = 'country_sort_reverse'; + static const stateSortReverseKey = 'state_sort_reverse'; static const placeSortReverseKey = 'place_sort_reverse'; static const tagSortReverseKey = 'tag_sort_reverse'; static const pinnedFiltersKey = 'pinned_filters'; @@ -245,6 +247,10 @@ class Settings extends ChangeNotifier { final performanceClass = await deviceService.getPerformanceClass(); enableBlurEffect = performanceClass >= 29; + final androidInfo = await DeviceInfoPlugin().androidInfo; + final pattern = BurstPatterns.byManufacturer[androidInfo.manufacturer]; + collectionBurstPatterns = pattern != null ? [pattern] : []; + // availability if (flavor.hasMapStyleDefault) { final defaultMapStyle = mobileServices.defaultMapStyle; @@ -495,6 +501,10 @@ class Settings extends ChangeNotifier { // collection + List<String> get collectionBurstPatterns => getStringList(collectionBurstPatternsKey) ?? []; + + set collectionBurstPatterns(List<String> newValue) => _set(collectionBurstPatternsKey, newValue); + EntryGroupFactor get collectionSectionFactor => getEnumOrDefault(collectionGroupFactorKey, SettingsDefaults.collectionSectionFactor, EntryGroupFactor.values); set collectionSectionFactor(EntryGroupFactor newValue) => _set(collectionGroupFactorKey, newValue.toString()); @@ -549,19 +559,23 @@ class Settings extends ChangeNotifier { set albumGroupFactor(AlbumChipGroupFactor newValue) => _set(albumGroupFactorKey, newValue.toString()); - ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, SettingsDefaults.albumSortFactor, ChipSortFactor.values); + ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, SettingsDefaults.chipListSortFactor, ChipSortFactor.values); set albumSortFactor(ChipSortFactor newValue) => _set(albumSortFactorKey, newValue.toString()); - ChipSortFactor get countrySortFactor => getEnumOrDefault(countrySortFactorKey, SettingsDefaults.countrySortFactor, ChipSortFactor.values); + ChipSortFactor get countrySortFactor => getEnumOrDefault(countrySortFactorKey, SettingsDefaults.chipListSortFactor, ChipSortFactor.values); set countrySortFactor(ChipSortFactor newValue) => _set(countrySortFactorKey, newValue.toString()); - ChipSortFactor get placeSortFactor => getEnumOrDefault(placeSortFactorKey, SettingsDefaults.placeSortFactor, ChipSortFactor.values); + ChipSortFactor get stateSortFactor => getEnumOrDefault(stateSortFactorKey, SettingsDefaults.chipListSortFactor, ChipSortFactor.values); + + set stateSortFactor(ChipSortFactor newValue) => _set(stateSortFactorKey, newValue.toString()); + + ChipSortFactor get placeSortFactor => getEnumOrDefault(placeSortFactorKey, SettingsDefaults.chipListSortFactor, ChipSortFactor.values); set placeSortFactor(ChipSortFactor newValue) => _set(placeSortFactorKey, newValue.toString()); - ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, SettingsDefaults.tagSortFactor, ChipSortFactor.values); + ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, SettingsDefaults.chipListSortFactor, ChipSortFactor.values); set tagSortFactor(ChipSortFactor newValue) => _set(tagSortFactorKey, newValue.toString()); @@ -573,6 +587,10 @@ class Settings extends ChangeNotifier { set countrySortReverse(bool newValue) => _set(countrySortReverseKey, newValue); + bool get stateSortReverse => getBool(stateSortReverseKey) ?? false; + + set stateSortReverse(bool newValue) => _set(stateSortReverseKey, newValue); + bool get placeSortReverse => getBool(placeSortReverseKey) ?? false; set placeSortReverse(bool newValue) => _set(placeSortReverseKey, newValue); @@ -1074,6 +1092,7 @@ class Settings extends ChangeNotifier { case showThumbnailVideoDurationKey: case albumSortReverseKey: case countrySortReverseKey: + case stateSortReverseKey: case placeSortReverseKey: case tagSortReverseKey: case showOverlayOnOpeningKey: @@ -1122,6 +1141,7 @@ class Settings extends ChangeNotifier { case albumGroupFactorKey: case albumSortFactorKey: case countrySortFactorKey: + case stateSortFactorKey: case placeSortFactorKey: case tagSortFactorKey: case imageBackgroundKey: @@ -1152,6 +1172,7 @@ class Settings extends ChangeNotifier { case drawerTypeBookmarksKey: case drawerAlbumBookmarksKey: case drawerPageBookmarksKey: + case collectionBurstPatternsKey: case pinnedFiltersKey: case hiddenFiltersKey: case collectionBrowsingQuickActionsKey: diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 7b4e9c48c..e4524418e 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -6,7 +6,8 @@ import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/collection_utils.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; @@ -173,27 +174,13 @@ mixin AlbumMixin on SourceBase { final type = androidFileUtils.getAlbumType(dirPath); if (context != null) { - 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.vault: - case AlbumType.app: - break; - } + final name = type.getName(context); + if (name != null) return name; } if (type == AlbumType.vault) return pContext.basename(dirPath); - final dir = VolumeRelativeDirectory.fromPath(dirPath); + final dir = androidFileUtils.relativeDirectoryFromPath(dirPath); if (dir == null) return dirPath; final relativeDir = dir.relativeDir; diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index a117a8fdc..44b37f548 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:collection'; -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/sort.dart'; @@ -15,12 +14,12 @@ 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/enums/enums.dart'; import 'package:aves/model/source/events.dart'; import 'package:aves/model/source/location/location.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/collection_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_utils/aves_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -28,6 +27,7 @@ import 'package:flutter/foundation.dart'; class CollectionLens with ChangeNotifier { final CollectionSource source; final Set<CollectionFilter> filters; + List<String> burstPatterns; EntryGroupFactor sectionFactor; EntrySortFactor sortFactor; bool sortReverse; @@ -50,6 +50,7 @@ class CollectionLens with ChangeNotifier { this.fixedSort = false, this.fixedSelection, }) : filters = (filters ?? {}).whereNotNull().toSet(), + burstPatterns = settings.collectionBurstPatterns, sectionFactor = settings.collectionSectionFactor, sortFactor = settings.collectionSortFactor, sortReverse = settings.collectionSortReverse { @@ -85,6 +86,7 @@ class CollectionLens with ChangeNotifier { } _subscriptions.add(settings.updateStream .where((event) => [ + Settings.collectionBurstPatternsKey, Settings.collectionSortFactorKey, Settings.collectionGroupFactorKey, Settings.collectionSortReverseKey, @@ -188,7 +190,7 @@ class CollectionLens with ChangeNotifier { } void _groupBursts() { - final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.burstKey).whereNotNullKey(); + final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.getBurstKey(burstPatterns)).whereNotNullKey(); byBurstKey.forEach((burstKey, entries) { if (entries.length > 1) { entries.sort(AvesEntrySort.compareByName); @@ -287,13 +289,19 @@ class CollectionLens with ChangeNotifier { } void _onSettingsChanged() { + final newBurstPatterns = settings.collectionBurstPatterns; final newSortFactor = settings.collectionSortFactor; final newSectionFactor = settings.collectionSectionFactor; final newSortReverse = settings.collectionSortReverse; - final needSort = sortFactor != newSortFactor || sortReverse != newSortReverse; + final needFilter = burstPatterns != newBurstPatterns; + final needSort = needFilter || sortFactor != newSortFactor || sortReverse != newSortReverse; final needSection = needSort || sectionFactor != newSectionFactor; + if (needFilter) { + burstPatterns = newBurstPatterns; + _applyFilters(); + } if (needSort) { sortFactor = newSortFactor; sortReverse = newSortReverse; @@ -303,6 +311,10 @@ class CollectionLens with ChangeNotifier { sectionFactor = newSectionFactor; _applySection(); } + + if (needFilter) { + filterChangeNotifier.notifyListeners(); + } if (needSort || needSection) { sortSectionChangeNotifier.notifyListeners(); } @@ -316,9 +328,9 @@ class CollectionLens with ChangeNotifier { if (groupBursts) { // find impacted burst groups final obsoleteBurstEntries = <AvesEntry>{}; - final burstKeys = entries.map((entry) => entry.burstKey).whereNotNull().toSet(); + final burstKeys = entries.map((entry) => entry.getBurstKey(burstPatterns)).whereNotNull().toSet(); if (burstKeys.isNotEmpty) { - _filteredSortedEntries.where((entry) => entry.isBurst && burstKeys.contains(entry.burstKey)).forEach((mainEntry) { + _filteredSortedEntries.where((entry) => entry.isBurst && burstKeys.contains(entry.getBurstKey(burstPatterns))).forEach((mainEntry) { final subEntries = mainEntry.burstEntries!; // remove the deleted sub-entries subEntries.removeWhere(entries.contains); diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index e2925ac74..ec7d49304 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/covers.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/catalog.dart'; @@ -16,17 +15,18 @@ 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'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/events.dart'; import 'package:aves/model/source/location/country.dart'; import 'package:aves/model/source/location/location.dart'; import 'package:aves/model/source/location/place.dart'; +import 'package:aves/model/source/location/state.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/trash.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/analysis_service.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; @@ -59,7 +59,7 @@ mixin SourceBase { void invalidateEntries(); } -abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, LocationMixin, TagMixin, TrashMixin { +abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin { CollectionSource() { settings.updateStream.where((event) => event.key == Settings.localeKey).listen((_) => invalidateAlbumDisplayNames()); settings.updateStream.where((event) => event.key == Settings.hiddenFiltersKey).listen((event) { @@ -142,6 +142,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place invalidateAlbumFilterSummary(entries: entries, notify: notify); invalidateCountryFilterSummary(entries: entries, notify: notify); invalidatePlaceFilterSummary(entries: entries, notify: notify); + invalidateStateFilterSummary(entries: entries, notify: notify); invalidateTagFilterSummary(entries: entries, notify: notify); } @@ -511,6 +512,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place switch (filter.level) { case LocationLevel.country: return countryEntryCount(filter); + case LocationLevel.state: + return stateEntryCount(filter); case LocationLevel.place: return placeEntryCount(filter); } @@ -525,6 +528,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place switch (filter.level) { case LocationLevel.country: return countrySize(filter); + case LocationLevel.state: + return stateSize(filter); case LocationLevel.place: return placeSize(filter); } @@ -539,6 +544,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place switch (filter.level) { case LocationLevel.country: return countryRecentEntry(filter); + case LocationLevel.state: + return stateRecentEntry(filter); case LocationLevel.place: return placeRecentEntry(filter); } diff --git a/lib/model/source/events.dart b/lib/model/source/events.dart index 2cd779f31..ec8dc28d6 100644 --- a/lib/model/source/events.dart +++ b/lib/model/source/events.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/entry/entry.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; @immutable diff --git a/lib/model/source/location/country.dart b/lib/model/source/location/country.dart index 2fcce4db0..a8128dc43 100644 --- a/lib/model/source/location/country.dart +++ b/lib/model/source/location/country.dart @@ -5,8 +5,6 @@ import 'package:aves/utils/collection_utils.dart'; import 'package:collection/collection.dart'; mixin CountryMixin on SourceBase { - // filter summary - // by country code final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {}; final Map<String, AvesEntry?> _filterRecentEntryMap = {}; @@ -39,19 +37,19 @@ mixin CountryMixin on SourceBase { } int countryEntryCount(LocationFilter filter) { - final countryCode = filter.countryCode; + final countryCode = filter.code; if (countryCode == null) return 0; return _filterEntryCountMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).length); } int countrySize(LocationFilter filter) { - final countryCode = filter.countryCode; + final countryCode = filter.code; if (countryCode == null) return 0; return _filterSizeMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum); } AvesEntry? countryRecentEntry(LocationFilter filter) { - final countryCode = filter.countryCode; + final countryCode = filter.code; if (countryCode == null) return null; return _filterRecentEntryMap.putIfAbsent(countryCode, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); } diff --git a/lib/model/source/location/location.dart b/lib/model/source/location/location.dart index 75fec0ede..d0dadab88 100644 --- a/lib/model/source/location/location.dart +++ b/lib/model/source/location/location.dart @@ -7,19 +7,21 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/location/country.dart'; import 'package:aves/model/source/location/place.dart'; +import 'package:aves/model/source/location/state.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:tuple/tuple.dart'; -mixin LocationMixin on CountryMixin, PlaceMixin { +mixin LocationMixin on CountryMixin, StateMixin { static const commitCountThreshold = 200; static const _stopCheckCountThreshold = 50; List<String> sortedCountries = List.unmodifiable([]); + List<String> sortedStates = List.unmodifiable([]); List<String> sortedPlaces = List.unmodifiable([]); Future<void> loadAddresses({Set<int>? ids}) async { @@ -152,32 +154,56 @@ mixin LocationMixin on CountryMixin, PlaceMixin { void updateLocations() { final locations = visibleEntries.map((entry) => entry.addressDetails).whereNotNull().toList(); + final updatedPlaces = locations.map((address) => address.place).whereNotNull().where((v) => v.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase); if (!listEquals(updatedPlaces, sortedPlaces)) { sortedPlaces = List.unmodifiable(updatedPlaces); eventBus.fire(PlacesChangedEvent()); } - // the same country code could be found with different country names - // e.g. if the locale changed between geocoding calls - // so we merge countries by code, keeping only one name for each code - final countriesByCode = Map.fromEntries(locations.map((address) { - final code = address.countryCode; - if (code == null || code.isEmpty) return null; - return MapEntry(code, address.countryName); - }).whereNotNull()); - final updatedCountries = countriesByCode.entries.map((kv) { - final code = kv.key; - final name = kv.value; - return '${name != null && name.isNotEmpty ? name : code}${LocationFilter.locationSeparator}$code'; - }).toList() - ..sort(compareAsciiUpperCase); + final updatedStates = _getAreaByCode( + locations: locations, + getCode: (v) => v.stateCode, + getName: (v) => v.stateName, + ); + if (!listEquals(updatedStates, sortedStates)) { + sortedStates = List.unmodifiable(updatedStates); + invalidateStateFilterSummary(); + eventBus.fire(StatesChangedEvent()); + } + + final updatedCountries = _getAreaByCode( + locations: locations, + getCode: (v) => v.countryCode, + getName: (v) => v.countryName, + ); if (!listEquals(updatedCountries, sortedCountries)) { sortedCountries = List.unmodifiable(updatedCountries); invalidateCountryFilterSummary(); eventBus.fire(CountriesChangedEvent()); } } + + // the same country/state code could be found with different country/state names + // e.g. if the locale changed between geocoding calls + // so we merge countries/states by code, keeping only one name for each code + List<String> _getAreaByCode({ + required List<AddressDetails> locations, + required String? Function(AddressDetails address) getCode, + required String? Function(AddressDetails address) getName, + }) { + final namesByCode = Map.fromEntries(locations.map((address) { + final code = getCode(address); + if (code == null || code.isEmpty) return null; + return MapEntry(code, getName(address)); + }).whereNotNull()); + return namesByCode.entries.map((kv) { + final code = kv.key; + final name = kv.value; + return '${name != null && name.isNotEmpty ? name : code}${LocationFilter.locationSeparator}$code'; + }).toList() + ..sort(compareAsciiUpperCase); + } } class AddressMetadataChangedEvent {} diff --git a/lib/model/source/location/place.dart b/lib/model/source/location/place.dart index a0b1e3ab6..aad3f9824 100644 --- a/lib/model/source/location/place.dart +++ b/lib/model/source/location/place.dart @@ -5,8 +5,6 @@ import 'package:aves/utils/collection_utils.dart'; import 'package:collection/collection.dart'; mixin PlaceMixin on SourceBase { - // filter summary - // by place final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {}; final Map<String, AvesEntry?> _filterRecentEntryMap = {}; diff --git a/lib/model/source/location/state.dart b/lib/model/source/location/state.dart new file mode 100644 index 000000000..67ce98bda --- /dev/null +++ b/lib/model/source/location/state.dart @@ -0,0 +1,64 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/utils/collection_utils.dart'; +import 'package:collection/collection.dart'; + +mixin StateMixin on SourceBase { + // by state code + final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {}; + final Map<String, AvesEntry?> _filterRecentEntryMap = {}; + + void invalidateStateFilterSummary({ + Set<AvesEntry>? entries, + Set<String>? stateCodes, + bool notify = true, + }) { + if (_filterEntryCountMap.isEmpty && _filterSizeMap.isEmpty && _filterRecentEntryMap.isEmpty) return; + + if (entries == null && stateCodes == null) { + _filterEntryCountMap.clear(); + _filterSizeMap.clear(); + _filterRecentEntryMap.clear(); + } else { + stateCodes ??= {}; + if (entries != null) { + stateCodes.addAll(entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.stateCode).whereNotNull()); + } + stateCodes.forEach((stateCode) { + _filterEntryCountMap.remove(stateCode); + _filterSizeMap.remove(stateCode); + _filterRecentEntryMap.remove(stateCode); + }); + } + if (notify) { + eventBus.fire(StateSummaryInvalidatedEvent(stateCodes)); + } + } + + int stateEntryCount(LocationFilter filter) { + final stateCode = filter.code; + if (stateCode == null) return 0; + return _filterEntryCountMap.putIfAbsent(stateCode, () => visibleEntries.where(filter.test).length); + } + + int stateSize(LocationFilter filter) { + final stateCode = filter.code; + if (stateCode == null) return 0; + return _filterSizeMap.putIfAbsent(stateCode, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum); + } + + AvesEntry? stateRecentEntry(LocationFilter filter) { + final stateCode = filter.code; + if (stateCode == null) return null; + return _filterRecentEntryMap.putIfAbsent(stateCode, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); + } +} + +class StatesChangedEvent {} + +class StateSummaryInvalidatedEvent { + final Set<String>? stateCodes; + + const StateSummaryInvalidatedEvent(this.stateCodes); +} diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 4e1c17f2d..5ca9a2466 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -8,12 +8,12 @@ import 'package:aves/model/favourites.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; class MediaStoreSource extends CollectionSource { SourceInitializationState _initState = SourceInitializationState.none; diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 6b8dcd682..bd8248b07 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -4,9 +4,9 @@ import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/collection_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/model/source/trash.dart b/lib/model/source/trash.dart index 51c98dd2a..5aac6de6e 100644 --- a/lib/model/source/trash.dart +++ b/lib/model/source/trash.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aves/model/entry/extensions/props.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'; diff --git a/lib/model/vaults/details.dart b/lib/model/vaults/details.dart index d4898148d..4d1a165b8 100644 --- a/lib/model/vaults/details.dart +++ b/lib/model/vaults/details.dart @@ -1,6 +1,6 @@ -import 'package:aves/model/vaults/enums.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/collection_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/model/vaults/vaults.dart b/lib/model/vaults/vaults.dart index 902a9aa0d..da80ec112 100644 --- a/lib/model/vaults/vaults.dart +++ b/lib/model/vaults/vaults.dart @@ -2,19 +2,10 @@ import 'dart:async'; import 'dart:io'; import 'package:aves/model/vaults/details.dart'; -import 'package:aves/model/vaults/enums.dart'; import 'package:aves/services/common/services.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/password_dialog.dart'; -import 'package:aves/widgets/dialogs/filter_editors/pattern_dialog.dart'; -import 'package:aves/widgets/dialogs/filter_editors/pin_dialog.dart'; +import 'package:aves_screen_state/aves_screen_state.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:local_auth/error_codes.dart' as auth_error; -import 'package:local_auth/local_auth.dart'; -import 'package:screen_state/screen_state.dart'; +import 'package:flutter/foundation.dart'; final Vaults vaults = Vaults._private(); @@ -28,9 +19,9 @@ class Vaults extends ChangeNotifier { Future<void> init() async { _rows = await metadataDb.loadAllVaults(); _vaultDirPaths = null; - final screenStateStream = Platform.isAndroid ? Screen().screenStateStream : null; + final screenStateStream = Platform.isAndroid ? AvesScreenState().screenStateStream : null; if (screenStateStream != null) { - _subscriptions.add(screenStateStream.where((event) => event == ScreenStateEvent.SCREEN_OFF).listen((event) => _onScreenOff())); + _subscriptions.add(screenStateStream.where((event) => event == ScreenStateEvent.off).listen((event) => _onScreenOff())); } } @@ -44,7 +35,7 @@ class Vaults extends ChangeNotifier { Set<VaultDetails> get all => Set.unmodifiable(_rows); - VaultDetails? _detailsForPath(String dirPath) => _rows.firstWhereOrNull((v) => v.path == dirPath); + VaultDetails? detailsForPath(String dirPath) => _rows.firstWhereOrNull((v) => v.path == dirPath); Future<void> create(VaultDetails details) async { await metadataDb.addVaults({details}); @@ -56,7 +47,7 @@ class Vaults extends ChangeNotifier { } Future<void> remove(Set<String> dirPaths) async { - final details = dirPaths.map(_detailsForPath).whereNotNull().toSet(); + final details = dirPaths.map(detailsForPath).whereNotNull().toSet(); if (details.isEmpty) return; await metadataDb.removeVaults(details); @@ -70,7 +61,7 @@ class Vaults extends ChangeNotifier { } Future<void> rename(String oldDirPath, String newDirPath) async { - final oldDetails = _detailsForPath(oldDirPath); + final oldDetails = detailsForPath(oldDirPath); if (oldDetails == null) return; final newName = VaultDetails.nameFromPath(newDirPath); @@ -96,7 +87,7 @@ class Vaults extends ChangeNotifier { // update details, except name Future<void> update(VaultDetails newDetails) async { - final oldDetails = _detailsForPath(newDetails.path); + final oldDetails = detailsForPath(newDetails.path); if (oldDetails == null) return; await metadataDb.updateVault(newDetails.name, newDetails); @@ -141,119 +132,11 @@ class Vaults extends ChangeNotifier { _onLockStateChanged(); } - Future<bool> tryUnlock(String dirPath, BuildContext context) async { - if (!isVault(dirPath) || !isLocked(dirPath)) return true; - - final details = _detailsForPath(dirPath); - if (details == null) return false; - - bool? confirmed; - switch (details.lockType) { - case VaultLockType.system: - try { - confirmed = await LocalAuthentication().authenticate( - localizedReason: context.l10n.authenticateToUnlockVault, - ); - } on PlatformException catch (e, stack) { - if (e.code != 'auth_in_progress') { - // `auth_in_progress`: `Authentication in progress` - await reportService.recordError(e, stack); - } - } - break; - case VaultLockType.pattern: - final pattern = await showDialog<String>( - context: context, - builder: (context) => const PatternDialog(needConfirmation: false), - routeSettings: const RouteSettings(name: PatternDialog.routeName), - ); - if (pattern != null) { - confirmed = pattern == await securityService.readValue(details.passKey); - } - break; - case VaultLockType.pin: - final pin = await showDialog<String>( - context: context, - builder: (context) => const PinDialog(needConfirmation: false), - routeSettings: const RouteSettings(name: PinDialog.routeName), - ); - if (pin != null) { - confirmed = pin == await securityService.readValue(details.passKey); - } - break; - case VaultLockType.password: - final password = await showDialog<String>( - context: context, - builder: (context) => const PasswordDialog(needConfirmation: false), - routeSettings: const RouteSettings(name: PasswordDialog.routeName), - ); - if (password != null) { - confirmed = password == await securityService.readValue(details.passKey); - } - break; - } - - if (confirmed == null || !confirmed) return false; + void unlock(String dirPath) { + if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return; _unlockedDirPaths.add(dirPath); _onLockStateChanged(); - return true; - } - - Future<bool> setPass(BuildContext context, VaultDetails details) async { - switch (details.lockType) { - case VaultLockType.system: - final l10n = context.l10n; - try { - return await LocalAuthentication().authenticate( - localizedReason: l10n.authenticateToConfigureVault, - ); - } on PlatformException catch (e, stack) { - await showDialog( - context: context, - builder: (context) => AvesDialog( - content: Text(e.message ?? l10n.genericFailureFeedback), - actions: const [OkButton()], - ), - routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), - ); - if (e.code != auth_error.notAvailable) { - await reportService.recordError(e, stack); - } - } - break; - case VaultLockType.pattern: - final pattern = await showDialog<String>( - context: context, - builder: (context) => const PatternDialog(needConfirmation: true), - routeSettings: const RouteSettings(name: PatternDialog.routeName), - ); - if (pattern != null) { - return await securityService.writeValue(details.passKey, pattern); - } - break; - case VaultLockType.pin: - final pin = await showDialog<String>( - context: context, - builder: (context) => const PinDialog(needConfirmation: true), - routeSettings: const RouteSettings(name: PinDialog.routeName), - ); - if (pin != null) { - return await securityService.writeValue(details.passKey, pin); - } - break; - case VaultLockType.password: - final password = await showDialog<String>( - context: context, - builder: (context) => const PasswordDialog(needConfirmation: true), - routeSettings: const RouteSettings(name: PasswordDialog.routeName), - ); - if (password != null) { - return await securityService.writeValue(details.passKey, password); - } - break; - } - return false; } void _onScreenOff() => lock(all.where((v) => v.autoLockScreenOff).map((v) => v.path).toSet()); diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index 2691bb6fe..eab4c5031 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -211,6 +211,12 @@ class VideoMetadataFormatter { final captureFps = double.parse(value); save('Capture Frame Rate', '${roundToPrecision(captureFps, decimals: 3).toString()} FPS'); break; + case Keys.androidManufacturer: + save('Android Manufacturer', value); + break; + case Keys.androidModel: + save('Android Model', value); + break; case Keys.androidVersion: save('Android Version', value); break; @@ -316,6 +322,16 @@ class VideoMetadataFormatter { case Keys.minorVersion: if (value != '0') save('Minor Version', value); break; + case Keys.quicktimeLocationAccuracyHorizontal: + save('QuickTime Location Horizontal Accuracy', value); + break; + case Keys.quicktimeCreationDate: + case Keys.quicktimeLocationIso6709: + case Keys.quicktimeMake: + case Keys.quicktimeModel: + case Keys.quicktimeSoftware: + // redundant with `QuickTime Metadata` directory + break; case Keys.rotate: save('Rotation', '$value°'); break; @@ -346,6 +362,9 @@ class VideoMetadataFormatter { case Keys.width: save('Width', '$value pixels'); break; + case Keys.xiaomiSlowMoment: + save('Xiaomi Slow Moment', value); + break; default: save(key.toSentenceCase(), value.toString()); } diff --git a/lib/ref/brand_colors.dart b/lib/ref/brand_colors.dart index b175781b2..08dc7fe10 100644 --- a/lib/ref/brand_colors.dart +++ b/lib/ref/brand_colors.dart @@ -1,11 +1,11 @@ -import 'package:flutter/painting.dart'; +import 'dart:ui'; class BrandColors { - static const Color adobeAfterEffects = Color(0xFF9A9AFF); - static const Color adobeIllustrator = Color(0xFFFF9B00); - static const Color adobePhotoshop = Color(0xFF2DAAFF); - static const Color android = Color(0xFF3DDC84); - static const Color flutter = Color(0xFF47D1FD); + static const adobeAfterEffects = Color(0xFF9A9AFF); + static const adobeIllustrator = Color(0xFFFF9B00); + static const adobePhotoshop = Color(0xFF2DAAFF); + static const android = Color(0xFF3DDC84); + static const flutter = Color(0xFF47D1FD); static Color? get(String text) { switch (text.toLowerCase()) { diff --git a/lib/ref/bursts.dart b/lib/ref/bursts.dart new file mode 100644 index 000000000..727506b8c --- /dev/null +++ b/lib/ref/bursts.dart @@ -0,0 +1,42 @@ +class BurstPatterns { + static const samsung = r'^(\d{8}_\d{6})_(\d+)$'; + static const sony = r'^DSC_\d+_BURST(\d{17})(_COVER)?$'; + + static final options = [ + BurstPatterns.samsung, + BurstPatterns.sony, + ]; + + static String getName(String pattern) { + switch (pattern) { + case samsung: + return 'Samsung'; + case sony: + return 'Sony'; + default: + return pattern; + } + } + + static String getExample(String pattern) { + switch (pattern) { + case samsung: + return '20151021_072800_007'; + case sony: + return 'DSC_0007_BURST20151021072800123'; + default: + return '?'; + } + } + + static const byManufacturer = { + _Manufacturers.samsung: samsung, + _Manufacturers.sony: sony, + }; +} + +// values as returned by `DeviceInfoPlugin().androidInfo` +class _Manufacturers { + static const samsung = 'samsung'; + static const sony = 'sony'; +} diff --git a/lib/ref/exif.dart b/lib/ref/metadata/exif.dart similarity index 100% rename from lib/ref/exif.dart rename to lib/ref/metadata/exif.dart diff --git a/lib/ref/geotiff.dart b/lib/ref/metadata/geotiff.dart similarity index 100% rename from lib/ref/geotiff.dart rename to lib/ref/metadata/geotiff.dart diff --git a/lib/ref/iptc.dart b/lib/ref/metadata/iptc.dart similarity index 100% rename from lib/ref/iptc.dart rename to lib/ref/metadata/iptc.dart diff --git a/lib/ref/metadata/xmp.dart b/lib/ref/metadata/xmp.dart new file mode 100644 index 000000000..7548cdc13 --- /dev/null +++ b/lib/ref/metadata/xmp.dart @@ -0,0 +1,107 @@ +class XmpNamespaces { + static const acdsee = 'http://ns.acdsee.com/iptc/1.0/'; + static const adsmlat = 'http://adsml.org/xmlns/'; + static const avm = 'http://www.communicatingastronomy.org/avm/1.0/'; + static const camera = 'http://pix4d.com/camera/1.0/'; + static const cc = 'http://creativecommons.org/ns#'; + static const creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/'; + static const crd = 'http://ns.adobe.com/camera-raw-defaults/1.0/'; + static const crlcp = 'http://ns.adobe.com/camera-raw-embedded-lens-profile/1.0/'; + static const crs = 'http://ns.adobe.com/camera-raw-settings/1.0/'; + static const crss = 'http://ns.adobe.com/camera-raw-saved-settings/1.0/'; + static const darktable = 'http://darktable.sf.net/'; + static const dc = 'http://purl.org/dc/elements/1.1/'; + static const dcterms = 'http://purl.org/dc/terms/'; + static const dicom = 'http://ns.adobe.com/DICOM/'; + static const digiKam = 'http://www.digikam.org/ns/1.0/'; + static const droneDji = 'http://www.dji.com/drone-dji/1.0/'; + static const dwc = 'http://rs.tdwg.org/dwc/index.htm'; + static const dwciri = 'http://rs.tdwg.org/dwc/iri/'; + static const exif = 'http://ns.adobe.com/exif/1.0/'; + static const exifAux = 'http://ns.adobe.com/exif/1.0/aux/'; + static const exifEx = 'http://cipa.jp/exif/1.0/'; + static const fstop = 'http://www.fstopapp.com/xmp/'; + static const gAudio = 'http://ns.google.com/photos/1.0/audio/'; + static const gCamera = 'http://ns.google.com/photos/1.0/camera/'; + static const gContainer = 'http://ns.google.com/photos/1.0/container/'; + static const gCreations = 'http://ns.google.com/photos/1.0/creations/'; + static const gDepth = 'http://ns.google.com/photos/1.0/depthmap/'; + static const gDevice = 'http://ns.google.com/photos/dd/1.0/device/'; + static const gDeviceCamera = 'http://ns.google.com/photos/dd/1.0/camera/'; + static const gDeviceContainer = 'http://ns.google.com/photos/dd/1.0/container/'; + static const gDeviceItem = 'http://ns.google.com/photos/dd/1.0/item/'; + static const gFocus = 'http://ns.google.com/photos/1.0/focus/'; + static const gImage = 'http://ns.google.com/photos/1.0/image/'; + static const gPano = 'http://ns.google.com/photos/1.0/panorama/'; + static const gSpherical = 'http://ns.google.com/videos/1.0/spherical/'; + static const gettyImagesGift = 'http://xmp.gettyimages.com/gift/1.0/'; + static const gimp210 = 'http://www.gimp.org/ns/2.10/'; + static const gimpXmp = 'http://www.gimp.org/xmp/'; + static const illustrator = 'http://ns.adobe.com/illustrator/1.0/'; + static const iptc4xmpCore = 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'; + static const iptc4xmpExt = 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/'; + static const lr = 'http://ns.adobe.com/lightroom/1.0/'; + static const mediapro = 'http://ns.iview-multimedia.com/mediapro/1.0/'; + static const miCamera = 'http://ns.xiaomi.com/photos/1.0/camera/'; + + // also seen in the wild for prefix `MicrosoftPhoto`: 'http://ns.microsoft.com/photo/1.0' + static const microsoftPhoto = 'http://ns.microsoft.com/photo/1.0/'; + static const mp1 = 'http://ns.microsoft.com/photo/1.1'; + static const mp = 'http://ns.microsoft.com/photo/1.2/'; + static const mpri = 'http://ns.microsoft.com/photo/1.2/t/RegionInfo#'; + static const mpreg = 'http://ns.microsoft.com/photo/1.2/t/Region#'; + static const mwgrs = 'http://www.metadataworkinggroup.com/schemas/regions/'; + static const nga = 'https://standards.nga.gov/metadata/media/image/artobject/1.0'; + static const opMedia = 'http://ns.oneplus.com/media/1.0/'; + static const panorama = 'http://ns.adobe.com/photoshop/1.0/panorama-profile'; + static const panoStudio = 'http://www.tshsoft.com/xmlns'; + static const pdf = 'http://ns.adobe.com/pdf/1.3/'; + static const pdfX = 'http://ns.adobe.com/pdfx/1.3/'; + static const photoMechanic = 'http://ns.camerabits.com/photomechanic/1.0/'; + static const photoshop = 'http://ns.adobe.com/photoshop/1.0/'; + static const plus = 'http://ns.useplus.org/ldf/xmp/1.0/'; + static const pmtm = 'http://www.hdrsoft.com/photomatix_settings01'; + static const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + static const stCamera = 'http://ns.adobe.com/photoshop/1.0/camera-profile'; + static const stEvt = 'http://ns.adobe.com/xap/1.0/sType/ResourceEvent#'; + static const stRef = 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#'; + static const tiff = 'http://ns.adobe.com/tiff/1.0/'; + static const x = 'adobe:ns:meta/'; + static const xmp = 'http://ns.adobe.com/xap/1.0/'; + static const xmpBJ = 'http://ns.adobe.com/xap/1.0/bj/'; + static const xmpDM = 'http://ns.adobe.com/xmp/1.0/DynamicMedia/'; + static const xmpGImg = 'http://ns.adobe.com/xap/1.0/g/img/'; + static const xmpMM = 'http://ns.adobe.com/xap/1.0/mm/'; + static const xmpNote = 'http://ns.adobe.com/xmp/note/'; + static const xmpRights = 'http://ns.adobe.com/xap/1.0/rights/'; + static const xmpTPg = 'http://ns.adobe.com/xap/1.0/t/pg/'; + static const xperiaCamera = 'http://xmlns.sony.net/xperia/camera/1.0/'; +} + +class XmpElements { + static const xXmpmeta = 'xmpmeta'; + static const rdfRoot = 'RDF'; + static const rdfDescription = 'Description'; + static const containerDirectory = 'Directory'; + static const dcDescription = 'description'; + static const dcSubject = 'subject'; + static const dcTitle = 'title'; + static const msPhotoRating = 'Rating'; + static const xmpRating = 'Rating'; +} + +class XmpAttributes { + static const xXmptk = 'xmptk'; + static const rdfAbout = 'about'; + static const gCameraMicroVideo = 'MicroVideo'; + static const gCameraMicroVideoVersion = 'MicroVideoVersion'; + static const gCameraMicroVideoOffset = 'MicroVideoOffset'; + static const gCameraMicroVideoPresentationTimestampUs = 'MicroVideoPresentationTimestampUs'; + static const gCameraMotionPhoto = 'MotionPhoto'; + static const gCameraMotionPhotoVersion = 'MotionPhotoVersion'; + static const gCameraMotionPhotoPresentationTimestampUs = 'MotionPhotoPresentationTimestampUs'; + static const xmpCreateDate = 'CreateDate'; + static const xmpMetadataDate = 'MetadataDate'; + static const xmpModifyDate = 'ModifyDate'; + static const xmpNoteHasExtendedXMP = 'HasExtendedXMP'; +} diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart index 4a993fe42..8d2b7ea6c 100644 --- a/lib/ref/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -86,22 +86,9 @@ class MimeTypes { static const Set<String> rawImages = {arw, cr2, crw, dcr, dng, dngX, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f}; - // TODO TLAD [codec] make it dynamic if it depends on OS/lib versions - static const Set<String> undecodableImages = {art, cdr, crw, djvu, jpeg2000, jxl, pat, pcx, pnm, psdVnd, psdX, octetStream, zip}; + static bool canHaveAlpha(String mimeType) => MimeTypes.alphaImages.contains(mimeType); - static const Set<String> _knownOpaqueImages = {jpeg}; - - static const Set<String> _knownVideos = {v3gpp, asf, avi, aviMSVideo, aviVnd, aviXMSVideo, dvd, flv, flvX, mkv, mkvX, mov, movX, mp2p, mp2t, mp2ts, mp4, mpeg, ogv, realVideo, webm, wmv}; - - static final Set<String> knownMediaTypes = { - anyImage, - ..._knownOpaqueImages, - ...alphaImages, - ...rawImages, - ...undecodableImages, - anyVideo, - ..._knownVideos, - }; + static bool isRaw(String mimeType) => MimeTypes.rawImages.contains(mimeType); static bool isImage(String mimeType) => mimeType.startsWith('image'); @@ -147,56 +134,4 @@ class MimeTypes { } return null; } - - // `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes, - // and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files. - static bool canEditExif(String mimeType) { - switch (mimeType.toLowerCase()) { - // as of androidx.exifinterface:exifinterface:1.3.4 - case jpeg: - case png: - case webp: - return true; - default: - return false; - } - } - - static bool canEditIptc(String mimeType) { - switch (mimeType.toLowerCase()) { - // as of latest PixyMeta - case jpeg: - case tiff: - return true; - default: - return false; - } - } - - static bool canEditXmp(String mimeType) { - switch (mimeType.toLowerCase()) { - // as of latest PixyMeta - case gif: - case jpeg: - case png: - case tiff: - return true; - // using `mp4parser` - case mp4: - return true; - default: - return false; - } - } - - static bool canRemoveMetadata(String mimeType) { - switch (mimeType.toLowerCase()) { - // as of latest PixyMeta - case jpeg: - case tiff: - return true; - default: - return false; - } - } } diff --git a/lib/ref/poi.dart b/lib/ref/poi.dart new file mode 100644 index 000000000..1aebe9aae --- /dev/null +++ b/lib/ref/poi.dart @@ -0,0 +1,15 @@ +import 'package:latlong2/latlong.dart'; + +class PointsOfInterest { + static final pointNemo = LatLng(-48.876667, -123.393333); + + static final wonders = [ + LatLng(29.979167, 31.134167), + LatLng(36.451000, 28.223615), + LatLng(32.5355, 44.4275), + LatLng(31.213889, 29.885556), + LatLng(37.0379, 27.4241), + LatLng(37.637861, 21.63), + LatLng(37.949722, 27.363889), + ]; +} \ No newline at end of file diff --git a/lib/ref/unicode.dart b/lib/ref/unicode.dart new file mode 100644 index 000000000..9220d9fb2 --- /dev/null +++ b/lib/ref/unicode.dart @@ -0,0 +1,242 @@ +// cf Flutter's `foundation/unicode.dart` for bidi related characters +class UniChars { + static const noBreakSpace = '\u00A0'; + static const multiplicationSign = '\u00D7'; // × + static const emDash = '\u2014'; // — + static const bullet = '\u2022'; // • + static const ratio = '\u2236'; // ∶ + static const whiteMediumStar = '\u2B50'; // ⭐ +} + +class UniCodes { + // Block: Basic Latin + static const latinCapitalLetterA = 0x0041; + + // Block: Enclosed Alphanumeric Supplement + static const regionalIndicatorSymbolLetterA = 0x1F1E6; + + // Block: Miscellaneous Symbols and Pictographs + static const wavingBlackFlag = 0x1F3F4; + + // Block: Tags + static const tagLatinSmallLetterA = 0xE0061; + static const cancelTag = 0xE007F; +} + +class EmojiStateCodes { + // AU + static const auAustralianCapitalTerritory = 'auact'; + static const auNewSouthWales = 'aunsw'; + static const auNorthernTerritory = 'aunt'; + static const auQueensland = 'auqld'; + static const auSouthAustralia = 'ausa'; + static const auTasmania = 'autas'; + static const auVictoria = 'auvic'; + static const auWesternAustralia = 'auwa'; + + static const aus = { + auAustralianCapitalTerritory, + auNewSouthWales, + auNorthernTerritory, + auQueensland, + auSouthAustralia, + auTasmania, + auVictoria, + auWesternAustralia, + }; + + // GB + static const gbEngland = 'gbeng'; + static const gbNorthernIreland = 'gbnir'; + static const gbScotland = 'gbsct'; + static const gbWales = 'gbwls'; + + static const gbr = { + gbEngland, + gbNorthernIreland, + gbScotland, + gbWales, + }; + + // IN + static const inAndamanAndNicobarIslands = 'inan'; + static const inAndhraPradesh = 'inap'; + static const inArunachalPradesh = 'inar'; + static const inAssam = 'inas'; + static const inBihar = 'inbr'; + static const inChandigarh = 'inch'; + static const inChhattisgarh = 'inct'; + static const inDamanAndDiu = 'indd'; + static const inDelhi = 'indl'; + static const inDadraAndNagarHaveli = 'indn'; + static const inGoa = 'inga'; + static const inGujarat = 'ingj'; + static const inHimachalPradesh = 'inhp'; + static const inHaryana = 'inhr'; + static const inJharkhand = 'injh'; + static const inJammuAndKashmir = 'injk'; + static const inKarnataka = 'inka'; + static const inKerala = 'inkl'; + static const inLakshadweep = 'inld'; + static const inMaharashtra = 'inmh'; + static const inMeghalaya = 'inml'; + static const inManipur = 'inmn'; + static const inMadhyaPradesh = 'inmp'; + static const inMizoram = 'inmz'; + static const inNagaland = 'innl'; + static const inOdisha = 'inor'; + static const inPunjab = 'inpb'; + static const inPuducherry = 'inpy'; + static const inRajasthan = 'inrj'; + static const inSikkim = 'insk'; + static const inTelangana = 'intg'; + static const inTamilNadu = 'intn'; + static const inTripura = 'intr'; + static const inUttarPradesh = 'inup'; + static const inUttarakhand = 'inut'; + static const inWestBengal = 'inwb'; + + static const ind = { + inAndamanAndNicobarIslands, + inAndhraPradesh, + inArunachalPradesh, + inAssam, + inBihar, + inChandigarh, + inChhattisgarh, + inDamanAndDiu, + inDelhi, + inDadraAndNagarHaveli, + inGoa, + inGujarat, + inHimachalPradesh, + inHaryana, + inJharkhand, + inJammuAndKashmir, + inKarnataka, + inKerala, + inLakshadweep, + inMaharashtra, + inMeghalaya, + inManipur, + inMadhyaPradesh, + inMizoram, + inNagaland, + inOdisha, + inPunjab, + inPuducherry, + inRajasthan, + inSikkim, + inTelangana, + inTamilNadu, + inTripura, + inUttarPradesh, + inUttarakhand, + inWestBengal, + }; + + // US + static const usAlabama = 'usal'; + static const usAlaska = 'usak'; + static const usArizona = 'usaz'; + static const usArkansas = 'usar'; + static const usCalifornia = 'usca'; + static const usColorado = 'usco'; + static const usConnecticut = 'usct'; + static const usDelaware = 'usde'; + static const usFlorida = 'usfl'; + static const usGeorgia = 'usga'; + static const usHawaii = 'ushi'; + static const usIdaho = 'usid'; + static const usIllinois = 'usil'; + static const usIndiana = 'usin'; + static const usIowa = 'usia'; + static const usKansas = 'usks'; + static const usKentucky = 'usky'; + static const usLouisiana = 'usla'; + static const usMaine = 'usme'; + static const usMaryland = 'usmd'; + static const usMassachusetts = 'usma'; + static const usMichigan = 'usmi'; + static const usMinnesota = 'usmn'; + static const usMississippi = 'usms'; + static const usMissouri = 'usmo'; + static const usMontana = 'usmt'; + static const usNebraska = 'usne'; + static const usNevada = 'usnv'; + static const usNewHampshire = 'usnh'; + static const usNewJersey = 'usnj'; + static const usNewMexico = 'usnm'; + static const usNewYork = 'usny'; + static const usNorthCarolina = 'usnc'; + static const usNorthDakota = 'usnd'; + static const usOhio = 'usoh'; + static const usOklahoma = 'usok'; + static const usOregon = 'usor'; + static const usPennsylvania = 'uspa'; + static const usRhodeIsland = 'usri'; + static const usSouthCarolina = 'ussc'; + static const usSouthDakota = 'ussd'; + static const usTennessee = 'ustn'; + static const usUtah = 'usut'; + static const usVermont = 'usvt'; + static const usVirginia = 'usva'; + static const usWashington = 'uswa'; + static const usWashingtonDC = 'usdc'; + static const usWestVirginia = 'uswv'; + static const usWisconsin = 'uswi'; + static const usWyoming = 'uswy'; + + static const usa = { + usAlabama, + usAlaska, + usArizona, + usArkansas, + usCalifornia, + usColorado, + usConnecticut, + usDelaware, + usFlorida, + usGeorgia, + usHawaii, + usIdaho, + usIllinois, + usIndiana, + usIowa, + usKansas, + usKentucky, + usLouisiana, + usMaine, + usMaryland, + usMassachusetts, + usMichigan, + usMinnesota, + usMississippi, + usMissouri, + usMontana, + usNebraska, + usNevada, + usNewHampshire, + usNewJersey, + usNewMexico, + usNewYork, + usNorthCarolina, + usNorthDakota, + usOhio, + usOklahoma, + usOregon, + usPennsylvania, + usRhodeIsland, + usSouthCarolina, + usSouthDakota, + usTennessee, + usUtah, + usVermont, + usVirginia, + usWashington, + usWashingtonDC, + usWestVirginia, + usWisconsin, + usWyoming, + }; +} diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index 31a0c26e2..dde0d5675 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -5,11 +5,11 @@ import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/media_store_source.dart'; -import 'package:aves/model/source/source_state.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/services/android_app_service.dart b/lib/services/app_service.dart similarity index 97% rename from lib/services/android_app_service.dart rename to lib/services/app_service.dart index cf181a68b..14fc795ba 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/app_service.dart @@ -1,14 +1,14 @@ +import 'package:aves/model/apps.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/services/common/services.dart'; -import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; -abstract class AndroidAppService { +abstract class AppService { Future<Set<Package>> getPackages(); Future<Uint8List> getAppIcon(String packageName, double size); @@ -30,7 +30,7 @@ abstract class AndroidAppService { Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}); } -class PlatformAndroidAppService implements AndroidAppService { +class PlatformAppService implements AppService { static const _platform = MethodChannel('deckers.thibault/aves/app'); static final _knownAppDirs = { diff --git a/lib/services/common/services.dart b/lib/services/common/services.dart index 6fadb62e1..89ce1c47a 100644 --- a/lib/services/common/services.dart +++ b/lib/services/common/services.dart @@ -3,7 +3,7 @@ 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/app_service.dart'; import 'package:aves/services/device_service.dart'; import 'package:aves/services/media/embedded_data_service.dart'; import 'package:aves/services/media/media_edit_service.dart'; @@ -31,7 +31,7 @@ final p.Context pContext = getIt<p.Context>(); final AvesAvailability availability = getIt<AvesAvailability>(); final MetadataDb metadataDb = getIt<MetadataDb>(); -final AndroidAppService androidAppService = getIt<AndroidAppService>(); +final AppService appService = getIt<AppService>(); final DeviceService deviceService = getIt<DeviceService>(); final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>(); final MediaEditService mediaEditService = getIt<MediaEditService>(); @@ -51,7 +51,7 @@ void initPlatformServices() { getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new); getIt.registerLazySingleton<MetadataDb>(SqfliteMetadataDb.new); - getIt.registerLazySingleton<AndroidAppService>(PlatformAndroidAppService.new); + getIt.registerLazySingleton<AppService>(PlatformAppService.new); getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new); getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new); getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new); diff --git a/lib/services/geocoding_service.dart b/lib/services/geocoding_service.dart index 353cea173..15af95974 100644 --- a/lib/services/geocoding_service.dart +++ b/lib/services/geocoding_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:aves/services/common/services.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; @@ -35,9 +36,12 @@ class GeocodingService { } @immutable -class Address { +class Address extends Equatable { final String? addressLine, adminArea, countryCode, countryName, featureName, locality, postalCode, subAdminArea, subLocality, subThoroughfare, thoroughfare; + @override + List<Object?> get props => [addressLine, adminArea, countryCode, countryName, featureName, locality, postalCode, subAdminArea, subLocality, subThoroughfare, thoroughfare]; + const Address({ this.addressLine, this.adminArea, diff --git a/lib/services/global_search.dart b/lib/services/global_search.dart index 9df1b3835..c1b03e950 100644 --- a/lib/services/global_search.dart +++ b/lib/services/global_search.dart @@ -1,7 +1,9 @@ import 'dart:ui'; +import 'package:aves/model/entry/sort.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/format.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -53,7 +55,11 @@ Future<List<Map<String, String?>>> _getSuggestions(dynamic args) async { debugPrint('getSuggestions query=$query, locale=$locale use24hour=$use24hour'); if (query is String && locale is String) { - final entries = await metadataDb.searchLiveEntries(query, limit: 9); + final entries = (await metadataDb.searchLiveEntries(query, limit: 9)).toList(); + final catalogMetadata = await metadataDb.loadCatalogMetadataById(entries.map((entry) => entry.id).toSet()); + catalogMetadata.forEach((metadata) => entries.firstWhereOrNull((entry) => entry.id == metadata.id)?.catalogMetadata = metadata); + entries.sort(AvesEntrySort.compareByDate); + suggestions.addAll(entries.map((entry) { final date = entry.bestDate; return { diff --git a/lib/services/media/embedded_data_service.dart b/lib/services/media/embedded_data_service.dart index d4a668176..137144dca 100644 --- a/lib/services/media/embedded_data_service.dart +++ b/lib/services/media/embedded_data_service.dart @@ -1,6 +1,6 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/common/services.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/text.dart'; import 'package:flutter/services.dart'; abstract class EmbeddedDataService { @@ -42,7 +42,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - 'displayName': ['${entry.bestTitle}', dataUri].join(Constants.separator), + 'displayName': ['${entry.bestTitle}', dataUri].join(AText.separator), 'dataUri': dataUri, }); if (result != null) return result as Map; @@ -59,7 +59,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - 'displayName': ['${entry.bestTitle}', 'Image'].join(Constants.separator), + 'displayName': ['${entry.bestTitle}', 'Image'].join(AText.separator), }); if (result != null) return result as Map; } on PlatformException catch (e, stack) { @@ -75,7 +75,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - 'displayName': ['${entry.bestTitle}', 'Video'].join(Constants.separator), + 'displayName': ['${entry.bestTitle}', 'Video'].join(AText.separator), }); if (result != null) return result as Map; } on PlatformException catch (e, stack) { @@ -89,7 +89,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { try { final result = await _platform.invokeMethod('extractVideoEmbeddedPicture', <String, dynamic>{ 'uri': entry.uri, - 'displayName': ['${entry.bestTitle}', 'Cover'].join(Constants.separator), + 'displayName': ['${entry.bestTitle}', 'Cover'].join(AText.separator), }); if (result != null) return result as Map; } on PlatformException catch (e, stack) { @@ -105,7 +105,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - 'displayName': ['${entry.bestTitle}', '$props'].join(Constants.separator), + 'displayName': ['${entry.bestTitle}', '$props'].join(AText.separator), 'propPath': props, 'propMimeType': propMimeType, }); diff --git a/lib/services/media/media_edit_service.dart b/lib/services/media/media_edit_service.dart index 0ef3ba684..bfb4ab081 100644 --- a/lib/services/media/media_edit_service.dart +++ b/lib/services/media/media_edit_service.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:aves/model/entry/entry.dart'; -import 'package:aves/model/metadata/enums/enums.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_model/aves_model.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart index 4d6a9c4cd..e249024d2 100644 --- a/lib/services/media/media_fetch_service.dart +++ b/lib/services/media/media_fetch_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; +import 'package:aves/model/app/support.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/output_buffer.dart'; @@ -152,7 +153,7 @@ class PlatformMediaFetchService implements MediaFetchService { // `await` here, so that `completeError` will be caught below return await completer.future; } on PlatformException catch (e, stack) { - if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { + if (_isUnknownVisual(mimeType)) { await reportService.recordError(e, stack); } } @@ -191,7 +192,7 @@ class PlatformMediaFetchService implements MediaFetchService { }); if (result != null) return result as Uint8List; } on PlatformException catch (e, stack) { - if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { + if (_isUnknownVisual(mimeType)) { await reportService.recordError(e, stack); } } @@ -231,7 +232,7 @@ class PlatformMediaFetchService implements MediaFetchService { }); if (result != null) return result as Uint8List; } on PlatformException catch (e, stack) { - if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { + if (_isUnknownVisual(mimeType)) { await reportService.recordError(e, stack); } } @@ -259,4 +260,47 @@ class PlatformMediaFetchService implements MediaFetchService { @override Future<T>? resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey); + + // convenience methods + + bool _isUnknownVisual(String mimeType) => !_knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType); + + static const Set<String> _knownOpaqueImages = { + MimeTypes.jpeg, + }; + + static const Set<String> _knownVideos = { + MimeTypes.v3gpp, + MimeTypes.asf, + MimeTypes.avi, + MimeTypes.aviMSVideo, + MimeTypes.aviVnd, + MimeTypes.aviXMSVideo, + MimeTypes.dvd, + MimeTypes.flv, + MimeTypes.flvX, + MimeTypes.mkv, + MimeTypes.mkvX, + MimeTypes.mov, + MimeTypes.movX, + MimeTypes.mp2p, + MimeTypes.mp2t, + MimeTypes.mp2ts, + MimeTypes.mp4, + MimeTypes.mpeg, + MimeTypes.ogv, + MimeTypes.realVideo, + MimeTypes.webm, + MimeTypes.wmv, + }; + + static final Set<String> _knownMediaTypes = { + MimeTypes.anyImage, + ..._knownOpaqueImages, + ...MimeTypes.alphaImages, + ...MimeTypes.rawImages, + ...AppSupport.undecodableImages, + MimeTypes.anyVideo, + ..._knownVideos, + }; } diff --git a/lib/services/metadata/metadata_edit_service.dart b/lib/services/metadata/metadata_edit_service.dart index 53eb204a4..ec3d48bac 100644 --- a/lib/services/metadata/metadata_edit_service.dart +++ b/lib/services/metadata/metadata_edit_service.dart @@ -1,11 +1,11 @@ import 'dart:async'; +import 'package:aves/convert/convert.dart'; import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/metadata/date_modifier.dart'; -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/enums/metadata_type.dart'; -import 'package:aves/model/metadata/fields.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; @@ -123,7 +123,7 @@ class PlatformMetadataEditService implements MetadataEditService { } Future<void> _processPlatformException(AvesEntry entry, PlatformException e, StackTrace stack) async { - if (!entry.isMissingAtPath) { + if (entry.isValid) { final code = e.code; if (code.endsWith('mp4largemoov')) { await reportService.recordError(_Mp4LargeMoovException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart index 4998a1b6c..bbd5d7b30 100644 --- a/lib/services/metadata/metadata_fetch_service.dart +++ b/lib/services/metadata/metadata_fetch_service.dart @@ -1,9 +1,9 @@ +import 'package:aves/convert/convert.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/geotiff.dart'; import 'package:aves/model/metadata/catalog.dart'; -import 'package:aves/model/metadata/fields.dart'; import 'package:aves/model/metadata/overlay.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/panorama.dart'; @@ -12,6 +12,7 @@ import 'package:aves/services/common/service_policy.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/xmp.dart'; import 'package:aves/utils/time_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -57,7 +58,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { }); if (result != null) return result as Map; } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -95,7 +96,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { result['id'] = entry.id; return CatalogMetadata.fromMap(result); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -123,7 +124,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { }) as Map; return OverlayMetadata.fromMap(result); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -140,7 +141,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { }) as Map; return GeoTiffInfo.fromMap(result); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -165,7 +166,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { } return MultiPageInfo.fromPageMaps(entry, pageMaps); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -185,7 +186,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { }) as Map; return PanoramaInfo.fromMap(result); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -201,7 +202,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { }); if (result != null) return (result as List).cast<Map>().map((fields) => fields.cast<String, dynamic>()).toList(); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -218,7 +219,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { }); if (result != null) return AvesXmp.fromList((result as List).cast<String>()); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -253,7 +254,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { 'prop': prop, }); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -273,7 +274,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { return dateTimeFromMillis(result, isUtc: false); } } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } @@ -289,7 +290,7 @@ class PlatformMetadataFetchService implements MetadataFetchService { 'sizeBytes': entry.sizeBytes, }); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { + if (entry.isValid) { await reportService.recordError(e, stack); } } diff --git a/lib/services/metadata/svg_metadata_service.dart b/lib/services/metadata/svg_metadata_service.dart index 882b3527b..677bb97dc 100644 --- a/lib/services/metadata/svg_metadata_service.dart +++ b/lib/services/metadata/svg_metadata_service.dart @@ -1,10 +1,11 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:xml/xml.dart'; class SvgMetadataService { diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 2718acb32..674f0433e 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -1,8 +1,9 @@ import 'dart:async'; +import 'package:aves/model/covers.dart'; import 'package:aves/services/common/output_buffer.dart'; import 'package:aves/services/common/services.dart'; -import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; @@ -22,12 +23,12 @@ abstract class StorageService { Future<void> revokeDirectoryAccess(String path); // returns number of deleted directories - Future<int> deleteEmptyDirectories(Iterable<String> dirPaths); + Future<int> deleteEmptyRegularDirectories(Set<String> dirPaths); // returns whether user granted access to a directory of his choosing Future<bool> requestDirectoryAccess(String path); - Future<bool> canRequestMediaFileAccess(); + Future<bool> canRequestMediaFileBulkAccess(); Future<bool> canInsertMedia(Set<VolumeRelativeDirectory> directories); @@ -132,10 +133,10 @@ class PlatformStorageService implements StorageService { // returns number of deleted directories @override - Future<int> deleteEmptyDirectories(Iterable<String> dirPaths) async { + Future<int> deleteEmptyRegularDirectories(Set<String> dirPaths) async { try { final result = await _platform.invokeMethod('deleteEmptyDirectories', <String, dynamic>{ - 'dirPaths': dirPaths.toList(), + 'dirPaths': dirPaths.where((v) => covers.effectiveAlbumType(v) == AlbumType.regular).toList(), }); if (result != null) return result as int; } on PlatformException catch (e, stack) { @@ -145,7 +146,7 @@ class PlatformStorageService implements StorageService { } @override - Future<bool> canRequestMediaFileAccess() async { + Future<bool> canRequestMediaFileBulkAccess() async { try { final result = await _platform.invokeMethod('canRequestMediaFileBulkAccess'); if (result != null) return result as bool; diff --git a/lib/services/wallpaper_service.dart b/lib/services/wallpaper_service.dart index bff7e2152..3a2a4bd80 100644 --- a/lib/services/wallpaper_service.dart +++ b/lib/services/wallpaper_service.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/wallpaper_target.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/services.dart'; class WallpaperService { diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 357067b44..95ec57532 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -1,12 +1,22 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/covers.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:provider/provider.dart'; +class AColors { + static const starEnabled = Colors.amber; + static const starDisabled = Colors.grey; + + static const boraBoraGradient = [ + Color(0xff2bc0e4), + Color(0xffeaecc6), + ]; +} + class AvesColorsProvider extends StatelessWidget { final Widget child; @@ -33,6 +43,10 @@ class AvesColorsProvider extends StatelessWidget { } abstract class AvesColorsData { + static const defaultAccent = Colors.indigoAccent; + static const _neutralOnDark = Colors.white; + static const _neutralOnLight = Color(0xAA000000); + Color get neutral; Color fromHue(double hue); @@ -71,9 +85,6 @@ abstract class AvesColorsData { void clearAppColor(String album) => _appColors.remove(album); - static const Color _neutralOnDark = Colors.white; - static const Color _neutralOnLight = Color(0xAA000000); - // mime Color get image => fromHue(243); diff --git a/lib/theme/format.dart b/lib/theme/format.dart index 854299a08..89ef8ed03 100644 --- a/lib/theme/format.dart +++ b/lib/theme/format.dart @@ -1,4 +1,4 @@ -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/text.dart'; import 'package:intl/intl.dart'; String formatDay(DateTime date, String locale) => DateFormat.yMMMd(locale).format(date); @@ -8,7 +8,7 @@ String formatTime(DateTime date, String locale, bool use24hour) => (use24hour ? String formatDateTime(DateTime date, String locale, bool use24hour) => [ formatDay(date, locale), formatTime(date, locale, use24hour), - ].join(Constants.separator); + ].join(AText.separator); String formatFriendlyDuration(Duration d) { final seconds = (d.inSeconds.remainder(Duration.secondsPerMinute)).toString().padLeft(2, '0'); diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 35a2d3fe9..4c683e908 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -37,6 +37,7 @@ class AIcons { static const IconData location = Icons.place_outlined; static const IconData locationUnlocated = Icons.location_off_outlined; static const IconData country = Icons.flag_outlined; + static const IconData state = Icons.flag_outlined; static const IconData place = Icons.place_outlined; static const IconData mainStorage = Icons.smartphone_outlined; static const IconData mimeType = Icons.code_outlined; @@ -138,6 +139,8 @@ class AIcons { static const IconData vaultConfigure = MdiIcons.shieldLockOutline; static const IconData videoSettings = Icons.video_settings_outlined; static const IconData view = Icons.grid_view_outlined; + static const IconData viewerLock = Icons.lock_outline; + static const IconData viewerUnlock = Icons.lock_open_outlined; static const IconData zoomIn = Icons.add_outlined; static const IconData zoomOut = Icons.remove_outlined; static const IconData collapse = Icons.expand_less_outlined; diff --git a/lib/theme/styles.dart b/lib/theme/styles.dart new file mode 100644 index 000000000..bd8b584f2 --- /dev/null +++ b/lib/theme/styles.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +import 'package:flutter/painting.dart'; + +class AStyles { + // as of Flutter v2.8.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped + // so we give it a `strutStyle` with a slightly larger height + static const overflowStrut = StrutStyle(height: 1.3); + + static const knownTitleText = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w300, + fontFeatures: [FontFeature.enable('smcp')], + ); + + static TextStyle unknownTitleText = knownTitleText; + + static void updateStylesForLocale(Locale locale) { + final smcp = locale.languageCode != 'el'; + unknownTitleText = smcp ? knownTitleText : knownTitleText.copyWith(fontFeatures: []); + } + + static const embossShadows = [ + Shadow( + color: Color(0xFF000000), + offset: Offset(0.5, 1.0), + ) + ]; +} diff --git a/lib/theme/text.dart b/lib/theme/text.dart new file mode 100644 index 000000000..b7840d33f --- /dev/null +++ b/lib/theme/text.dart @@ -0,0 +1,7 @@ +import 'package:aves/ref/unicode.dart'; + +class AText { + static const separator = ' ${UniChars.bullet} '; + static const resolutionSeparator = ' ${UniChars.multiplicationSign} '; + static const valueNotAvailable = UniChars.emDash; +} \ No newline at end of file diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart index c23832fdf..30579055c 100644 --- a/lib/theme/themes.dart +++ b/lib/theme/themes.dart @@ -1,14 +1,12 @@ import 'dart:ui'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/aves_app.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class Themes { - static const defaultAccent = Colors.indigoAccent; - static const _tooltipTheme = TooltipThemeData( verticalOffset: 32, ); diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index d49784fb6..2368fd0dc 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -1,28 +1,33 @@ +import 'package:aves/model/apps.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { + // cf https://developer.android.com/reference/android/content/ContentResolver#SCHEME_CONTENT + static const contentScheme = 'content'; + + // cf https://developer.android.com/reference/android/provider/MediaStore#AUTHORITY + static const mediaStoreAuthority = 'media'; + + // cf https://developer.android.com/reference/android/provider/MediaStore#VOLUME_EXTERNAL + static const externalVolume = 'external'; + + static const mediaStoreUriRoot = '$contentScheme://$mediaStoreAuthority/'; + static const mediaUriPathRoots = {'/$externalVolume/images/', '/$externalVolume/video/'}; + static const String trashDirPath = '#trash'; late final String separator, vaultRoot, primaryStorage; late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath; late final Set<String> videoCapturesPaths; Set<StorageVolume> storageVolumes = {}; - Set<Package> _packages = {}; - List<String> _potentialAppDirs = []; bool _initialized = false; - ValueNotifier<bool> areAppNamesReadyNotifier = ValueNotifier(false); - - Iterable<Package> get _launcherPackages => _packages.where((package) => package.categoryLauncher); - AndroidFileUtils._private(); Future<void> init() async { @@ -58,21 +63,6 @@ class AndroidFileUtils { } } - Future<void> initAppNames() async { - if (_packages.isEmpty) { - debugPrint('Access installed app inventory'); - _packages = await androidAppService.getPackages(); - _potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList(); - areAppNamesReadyNotifier.value = true; - } - } - - Future<void> resetAppNames() async { - _packages.clear(); - _potentialAppDirs.clear(); - areAppNamesReadyNotifier.value = false; - } - bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('${separator}Camera') || path.endsWith('${separator}100ANDRO')); bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('${separator}Screenshots'); @@ -91,6 +81,19 @@ class AndroidFileUtils { return volume != null || path.endsWith(separator) ? volume : getStorageVolume('$path$separator'); } + // prefer static method over a null returning factory constructor + VolumeRelativeDirectory? relativeDirectoryFromPath(String dirPath) { + final volume = getStorageVolume(dirPath); + if (volume == null) return null; + + final root = volume.path; + final rootLength = root.length; + return VolumeRelativeDirectory( + volumePath: root, + relativeDir: dirPath.length < rootLength ? '' : dirPath.substring(rootLength), + ); + } + bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; AlbumType getAlbumType(String dirPath) { @@ -103,147 +106,8 @@ class AndroidFileUtils { if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures; final dir = pContext.split(dirPath).last; - if (dirPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; + if (dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app; return AlbumType.regular; } - - String? getAlbumAppPackageName(String albumPath) { - final dir = pContext.split(albumPath).last; - final package = _launcherPackages.firstWhereOrNull((package) => package.potentialDirs.contains(dir)); - return package?.packageName; - } - - String? getCurrentAppName(String packageName) { - final package = _packages.firstWhereOrNull((package) => package.packageName == packageName); - return package?.currentLabel; - } -} - -enum AlbumType { - regular, - vault, - app, - camera, - download, - screenRecordings, - screenshots, - videoCaptures, -} - -class Package { - final String packageName; - final String? currentLabel, englishLabel; - final bool categoryLauncher, isSystem; - final Set<String> ownedDirs = {}; - - Package({ - required this.packageName, - required this.currentLabel, - required this.englishLabel, - required this.categoryLauncher, - required this.isSystem, - }); - - factory Package.fromMap(Map map) { - return Package( - packageName: map['packageName'] ?? '', - currentLabel: map['currentLabel'], - englishLabel: map['englishLabel'], - categoryLauncher: map['categoryLauncher'] ?? false, - isSystem: map['isSystem'] ?? false, - ); - } - - Set<String> get potentialDirs => [ - currentLabel, - englishLabel, - ...ownedDirs, - ].whereNotNull().toSet(); - - @override - String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}'; -} - -@immutable -class StorageVolume extends Equatable { - final String? _description; - final String path, state; - final bool isPrimary, isRemovable; - - @override - List<Object?> get props => [_description, path, state, isPrimary, isRemovable]; - - const StorageVolume({ - required String? description, - required this.isPrimary, - required this.isRemovable, - required this.path, - required this.state, - }) : _description = description; - - String getDescription(BuildContext? context) { - if (_description != null) return _description!; - // ideally, the context should always be provided, but in some cases (e.g. album comparison), - // this would require numerous additional methods to have the context as argument - // for such a minor benefit: fallback volume description on Android < N - if (isPrimary) return context?.l10n.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage'; - return context?.l10n.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card'; - } - - factory StorageVolume.fromMap(Map map) { - final isPrimary = map['isPrimary'] ?? false; - return StorageVolume( - description: map['description'], - isPrimary: isPrimary, - isRemovable: map['isRemovable'] ?? false, - path: map['path'] ?? '', - state: map['state'] ?? '', - ); - } -} - -@immutable -class VolumeRelativeDirectory extends Equatable { - final String volumePath, relativeDir; - - @override - List<Object?> get props => [volumePath, relativeDir]; - - String get dirPath => '$volumePath$relativeDir'; - - const VolumeRelativeDirectory({ - required this.volumePath, - required this.relativeDir, - }); - - static VolumeRelativeDirectory fromMap(Map map) { - return VolumeRelativeDirectory( - volumePath: map['volumePath'] ?? '', - relativeDir: map['relativeDir'] ?? '', - ); - } - - Map<String, dynamic> toMap() => { - 'volumePath': volumePath, - 'relativeDir': relativeDir, - }; - - // prefer static method over a null returning factory constructor - static VolumeRelativeDirectory? fromPath(String dirPath) { - final volume = androidFileUtils.getStorageVolume(dirPath); - if (volume == null) return null; - - final root = volume.path; - final rootLength = root.length; - return VolumeRelativeDirectory( - volumePath: root, - relativeDir: dirPath.length < rootLength ? '' : dirPath.substring(rootLength), - ); - } - - String getVolumeDescription(BuildContext context) { - final volume = androidFileUtils.storageVolumes.firstWhereOrNull((volume) => volume.path == volumePath); - return volume?.getDescription(context) ?? volumePath; - } } diff --git a/lib/utils/colors.dart b/lib/utils/colors.dart new file mode 100644 index 000000000..d5e9768b4 --- /dev/null +++ b/lib/utils/colors.dart @@ -0,0 +1,8 @@ +import 'dart:ui'; + +class ColorUtils { + // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`) + // when used in gradients or lerping to it + static const transparentWhite = Color(0x00FFFFFF); + static const transparentBlack = Color(0x00000000); +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart deleted file mode 100644 index c47a3f4db..000000000 --- a/lib/utils/constants.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:permission_handler/permission_handler.dart'; - -class Constants { - static const storagePermissions = [ - Permission.storage, - // for media access on Android >=13 - Permission.photos, - Permission.videos, - ]; - - static const separator = ' • '; - - // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`) - // when used in gradients or lerping to it - static const transparentWhite = Color(0x00FFFFFF); - static const transparentBlack = Colors.transparent; - - // as of Flutter v2.8.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped - // so we give it a `strutStyle` with a slightly larger height - static const overflowStrutStyle = StrutStyle(height: 1.3); - - static const double colorPickerRadius = 16; - - static const knownTitleTextStyle = TextStyle( - fontSize: 20, - fontWeight: FontWeight.w300, - fontFeatures: [FontFeature.enable('smcp')], - ); - - static TextStyle unknownTitleTextStyle = knownTitleTextStyle; - - static void updateStylesForLocale(Locale locale) { - final smcp = locale.languageCode != 'el'; - unknownTitleTextStyle = smcp ? knownTitleTextStyle : knownTitleTextStyle.copyWith(fontFeatures: []); - } - - static const embossShadows = [ - Shadow( - color: Colors.black, - offset: Offset(0.5, 1.0), - ) - ]; - - static const boraBoraGradientColors = [ - Color(0xff2bc0e4), - Color(0xffeaecc6), - ]; - - // Bidi fun, cf https://www.unicode.org/reports/tr9/ - // First Strong Isolate - static const fsi = '\u2068'; - - // Pop Directional Isolate - static const pdi = '\u2069'; - - static const zwsp = '\u200B'; - - static const overlayUnknown = '—'; // em dash - - static final pointNemo = LatLng(-48.876667, -123.393333); - - static final wonders = [ - LatLng(29.979167, 31.134167), - LatLng(36.451000, 28.223615), - LatLng(32.5355, 44.4275), - LatLng(31.213889, 29.885556), - LatLng(37.0379, 27.4241), - LatLng(37.637861, 21.63), - LatLng(37.949722, 27.363889), - ]; - - static const int infoGroupMaxValueLength = 140; - - static const String avesGithub = 'https://github.com/deckerst/aves'; -} diff --git a/lib/utils/emoji_utils.dart b/lib/utils/emoji_utils.dart new file mode 100644 index 000000000..d3b06d49e --- /dev/null +++ b/lib/utils/emoji_utils.dart @@ -0,0 +1,22 @@ +import 'package:aves/ref/unicode.dart'; + +class EmojiUtils { + static const _countryCodeToFlagDiff = UniCodes.regionalIndicatorSymbolLetterA - UniCodes.latinCapitalLetterA; + static const _stateCodeToFlagDiff = UniCodes.tagLatinSmallLetterA - UniCodes.latinCapitalLetterA; + static const _subFlagStart = UniCodes.wavingBlackFlag; + static const _subFlagEnd = UniCodes.cancelTag; + + static String? countryCodeToFlag(String? code) { + if (code == null || code.length != 2) return null; + return String.fromCharCodes(code.toUpperCase().codeUnits.map((letter) => letter += _countryCodeToFlagDiff)); + } + + static String? stateCodeToFlag(String? code) { + if (code == null) return null; + return String.fromCharCodes([ + _subFlagStart, + ...code.toUpperCase().codeUnits.map((letter) => letter += _stateCodeToFlagDiff), + _subFlagEnd, + ]); + } +} diff --git a/lib/utils/xmp_utils.dart b/lib/utils/xmp_utils.dart index f660e51d6..c46274fa7 100644 --- a/lib/utils/xmp_utils.dart +++ b/lib/utils/xmp_utils.dart @@ -1,190 +1,29 @@ +import 'package:aves/ref/metadata/xmp.dart'; import 'package:intl/intl.dart'; import 'package:xml/xml.dart'; -class Namespaces { - static const acdsee = 'http://ns.acdsee.com/iptc/1.0/'; - static const adsmlat = 'http://adsml.org/xmlns/'; - static const avm = 'http://www.communicatingastronomy.org/avm/1.0/'; - static const camera = 'http://pix4d.com/camera/1.0/'; - static const cc = 'http://creativecommons.org/ns#'; - static const creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/'; - static const crd = 'http://ns.adobe.com/camera-raw-defaults/1.0/'; - static const crlcp = 'http://ns.adobe.com/camera-raw-embedded-lens-profile/1.0/'; - static const crs = 'http://ns.adobe.com/camera-raw-settings/1.0/'; - static const crss = 'http://ns.adobe.com/camera-raw-saved-settings/1.0/'; - static const darktable = 'http://darktable.sf.net/'; - static const dc = 'http://purl.org/dc/elements/1.1/'; - static const dcterms = 'http://purl.org/dc/terms/'; - static const dicom = 'http://ns.adobe.com/DICOM/'; - static const digiKam = 'http://www.digikam.org/ns/1.0/'; - static const droneDji = 'http://www.dji.com/drone-dji/1.0/'; - static const dwc = 'http://rs.tdwg.org/dwc/index.htm'; - static const dwciri = 'http://rs.tdwg.org/dwc/iri/'; - static const exif = 'http://ns.adobe.com/exif/1.0/'; - static const exifAux = 'http://ns.adobe.com/exif/1.0/aux/'; - static const exifEx = 'http://cipa.jp/exif/1.0/'; - static const gAudio = 'http://ns.google.com/photos/1.0/audio/'; - static const gCamera = 'http://ns.google.com/photos/1.0/camera/'; - static const gContainer = 'http://ns.google.com/photos/1.0/container/'; - static const gCreations = 'http://ns.google.com/photos/1.0/creations/'; - static const gDepth = 'http://ns.google.com/photos/1.0/depthmap/'; - static const gDevice = 'http://ns.google.com/photos/dd/1.0/device/'; - static const gDeviceCamera = 'http://ns.google.com/photos/dd/1.0/camera/'; - static const gDeviceContainer = 'http://ns.google.com/photos/dd/1.0/container/'; - static const gDeviceItem = 'http://ns.google.com/photos/dd/1.0/item/'; - static const gFocus = 'http://ns.google.com/photos/1.0/focus/'; - static const gImage = 'http://ns.google.com/photos/1.0/image/'; - static const gPano = 'http://ns.google.com/photos/1.0/panorama/'; - static const gSpherical = 'http://ns.google.com/videos/1.0/spherical/'; - static const gettyImagesGift = 'http://xmp.gettyimages.com/gift/1.0/'; - static const gimp210 = 'http://www.gimp.org/ns/2.10/'; - static const gimpXmp = 'http://www.gimp.org/xmp/'; - static const illustrator = 'http://ns.adobe.com/illustrator/1.0/'; - static const iptc4xmpCore = 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'; - static const iptc4xmpExt = 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/'; - static const lr = 'http://ns.adobe.com/lightroom/1.0/'; - static const mediapro = 'http://ns.iview-multimedia.com/mediapro/1.0/'; - static const miCamera = 'http://ns.xiaomi.com/photos/1.0/camera/'; - - // also seen in the wild for prefix `MicrosoftPhoto`: 'http://ns.microsoft.com/photo/1.0' - static const microsoftPhoto = 'http://ns.microsoft.com/photo/1.0/'; - static const mp1 = 'http://ns.microsoft.com/photo/1.1'; - static const mp = 'http://ns.microsoft.com/photo/1.2/'; - static const mpri = 'http://ns.microsoft.com/photo/1.2/t/RegionInfo#'; - static const mpreg = 'http://ns.microsoft.com/photo/1.2/t/Region#'; - static const mwgrs = 'http://www.metadataworkinggroup.com/schemas/regions/'; - static const nga = 'https://standards.nga.gov/metadata/media/image/artobject/1.0'; - static const opMedia = 'http://ns.oneplus.com/media/1.0/'; - static const panorama = 'http://ns.adobe.com/photoshop/1.0/panorama-profile'; - static const panoStudio = 'http://www.tshsoft.com/xmlns'; - static const pdf = 'http://ns.adobe.com/pdf/1.3/'; - static const pdfX = 'http://ns.adobe.com/pdfx/1.3/'; - static const photoMechanic = 'http://ns.camerabits.com/photomechanic/1.0/'; - static const photoshop = 'http://ns.adobe.com/photoshop/1.0/'; - static const plus = 'http://ns.useplus.org/ldf/xmp/1.0/'; - static const pmtm = 'http://www.hdrsoft.com/photomatix_settings01'; - static const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; - static const stCamera = 'http://ns.adobe.com/photoshop/1.0/camera-profile'; - static const stEvt = 'http://ns.adobe.com/xap/1.0/sType/ResourceEvent#'; - static const stRef = 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#'; - static const tiff = 'http://ns.adobe.com/tiff/1.0/'; - static const x = 'adobe:ns:meta/'; - static const xmp = 'http://ns.adobe.com/xap/1.0/'; - static const xmpBJ = 'http://ns.adobe.com/xap/1.0/bj/'; - static const xmpDM = 'http://ns.adobe.com/xmp/1.0/DynamicMedia/'; - static const xmpGImg = 'http://ns.adobe.com/xap/1.0/g/img/'; - static const xmpMM = 'http://ns.adobe.com/xap/1.0/mm/'; - static const xmpNote = 'http://ns.adobe.com/xmp/note/'; - static const xmpRights = 'http://ns.adobe.com/xap/1.0/rights/'; - static const xmpTPg = 'http://ns.adobe.com/xap/1.0/t/pg/'; - - // cf https://exiftool.org/TagNames/XMP.html - static const Map<String, String> nsTitles = { - acdsee: 'ACDSee', - adsmlat: 'AdsML', - exifAux: 'Exif Aux', - avm: 'Astronomy Visualization', - camera: 'Pix4D Camera', - cc: 'Creative Commons', - crd: 'Camera Raw Defaults', - creatorAtom: 'After Effects', - crs: 'Camera Raw Settings', - crss: 'Camera Raw Saved Settings', - darktable: 'darktable', - dc: 'Dublin Core', - digiKam: 'digiKam', - droneDji: 'DJI Drone', - dwc: 'Darwin Core', - exif: 'Exif', - exifEx: 'Exif Ex', - gAudio: 'Google Audio', - gCamera: 'Google Camera', - gContainer: 'Google Container', - gCreations: 'Google Creations', - gDepth: 'Google Depth', - gDevice: 'Google Device', - gFocus: 'Google Focus', - gImage: 'Google Image', - gPano: 'Google Panorama', - gSpherical: 'Google Spherical', - gettyImagesGift: 'Getty Images', - gimp210: 'GIMP 2.10', - gimpXmp: 'GIMP', - illustrator: 'Illustrator', - iptc4xmpCore: 'IPTC Core', - iptc4xmpExt: 'IPTC Extension', - lr: 'Lightroom', - mediapro: 'MediaPro', - miCamera: 'Mi Camera', - microsoftPhoto: 'Microsoft Photo 1.0', - mp1: 'Microsoft Photo 1.1', - mp: 'Microsoft Photo 1.2', - mwgrs: 'Regions', - nga: 'National Gallery of Art', - opMedia: 'OnePlus Media', - panorama: 'Panorama', - panoStudio: 'PanoramaStudio', - pdf: 'PDF', - pdfX: 'PDF/X', - photoMechanic: 'Photo Mechanic', - photoshop: 'Photoshop', - plus: 'PLUS', - pmtm: 'Photomatix', - tiff: 'TIFF', - xmp: 'Basic', - xmpBJ: 'Basic Job Ticket', - xmpDM: 'Dynamic Media', - xmpMM: 'Media Management', - xmpNote: 'Note', - xmpRights: 'Rights Management', - xmpTPg: 'Paged-Text', - }; - - static final defaultPrefixes = { - gContainer: 'Container', - dc: 'dc', - gCamera: 'GCamera', - microsoftPhoto: 'MicrosoftPhoto', - rdf: 'rdf', - x: 'x', - xmp: 'xmp', - xmpGImg: 'xmpGImg', - xmpNote: 'xmpNote', - }; -} - class XMP { static const xmlnsPrefix = 'xmlns'; static const propNamespaceSeparator = ':'; static const structFieldSeparator = '/'; - static String prefixOf(String ns) => Namespaces.defaultPrefixes[ns] ?? ''; + static final _defaultPrefixes = { + XmpNamespaces.gContainer: 'Container', + XmpNamespaces.dc: 'dc', + XmpNamespaces.gCamera: 'GCamera', + XmpNamespaces.microsoftPhoto: 'MicrosoftPhoto', + XmpNamespaces.rdf: 'rdf', + XmpNamespaces.x: 'x', + XmpNamespaces.xmp: 'xmp', + XmpNamespaces.xmpGImg: 'xmpGImg', + XmpNamespaces.xmpNote: 'xmpNote', + }; - // elements - static const xXmpmeta = 'xmpmeta'; - static const rdfRoot = 'RDF'; - static const rdfDescription = 'Description'; - static const containerDirectory = 'Directory'; - static const dcDescription = 'description'; - static const dcSubject = 'subject'; - static const dcTitle = 'title'; - static const msPhotoRating = 'Rating'; - static const xmpRating = 'Rating'; + static String prefixOf(String ns) => _defaultPrefixes[ns] ?? ''; - // attributes - static const xXmptk = 'xmptk'; - static const rdfAbout = 'about'; - static const gCameraMicroVideo = 'MicroVideo'; - static const gCameraMicroVideoVersion = 'MicroVideoVersion'; - static const gCameraMicroVideoOffset = 'MicroVideoOffset'; - static const gCameraMicroVideoPresentationTimestampUs = 'MicroVideoPresentationTimestampUs'; - static const gCameraMotionPhoto = 'MotionPhoto'; - static const gCameraMotionPhotoVersion = 'MotionPhotoVersion'; - static const gCameraMotionPhotoPresentationTimestampUs = 'MotionPhotoPresentationTimestampUs'; - static const xmpCreateDate = 'CreateDate'; - static const xmpMetadataDate = 'MetadataDate'; - static const xmpModifyDate = 'ModifyDate'; - static const xmpNoteHasExtendedXMP = 'HasExtendedXMP'; + static const nsRdf = XmpNamespaces.rdf; + static const nsX = XmpNamespaces.x; + static const nsXmp = XmpNamespaces.xmp; // for `rdf:Description` node only static bool _hasMeaningfulChildren(XmlNode node) => node.children.any((v) => v.nodeType != XmlNodeType.TEXT || v.text.trim().isNotEmpty); @@ -193,9 +32,9 @@ class XMP { static bool _hasMeaningfulAttributes(XmlNode description) { final hasMeaningfulAttributes = description.attributes.any((v) { switch (v.name.local) { - case rdfAbout: - case xmpMetadataDate: - case xmpModifyDate: + case XmpAttributes.rdfAbout: + case XmpAttributes.xmpMetadataDate: + case XmpAttributes.xmpModifyDate: return false; default: switch (v.name.prefix) { @@ -334,10 +173,10 @@ class XMP { node.children.add(rootBuilder.buildFragment()); final bagBuilder = XmlBuilder(); - bagBuilder.namespace(Namespaces.rdf, prefixOf(Namespaces.rdf)); - bagBuilder.element('Bag', namespace: Namespaces.rdf, nest: () { + bagBuilder.namespace(nsRdf, prefixOf(nsRdf)); + bagBuilder.element('Bag', namespace: nsRdf, nest: () { values.forEach((v) { - bagBuilder.element('li', namespace: Namespaces.rdf, nest: v); + bagBuilder.element('li', namespace: nsRdf, nest: v); }); }); node.children.last.children.add(bagBuilder.buildFragment()); @@ -359,42 +198,42 @@ class XMP { } if (xmpDoc == null) { final builder = XmlBuilder(); - builder.namespace(Namespaces.x, prefixOf(Namespaces.x)); - builder.element(xXmpmeta, namespace: Namespaces.x, namespaces: { - Namespaces.x: prefixOf(Namespaces.x), + builder.namespace(nsX, prefixOf(nsX)); + builder.element(XmpElements.xXmpmeta, namespace: nsX, namespaces: { + nsX: prefixOf(nsX), }, attributes: { - '${prefixOf(Namespaces.x)}$propNamespaceSeparator$xXmptk': await toolkit(), + '${prefixOf(nsX)}$propNamespaceSeparator${XmpAttributes.xXmptk}': await toolkit(), }); xmpDoc = builder.buildDocument(); } final root = xmpDoc.rootElement; - XmlNode? rdf = root.getElement(rdfRoot, namespace: Namespaces.rdf); + XmlNode? rdf = root.getElement(XmpElements.rdfRoot, namespace: nsRdf); if (rdf == null) { final builder = XmlBuilder(); - builder.namespace(Namespaces.rdf, prefixOf(Namespaces.rdf)); - builder.element(rdfRoot, namespace: Namespaces.rdf, namespaces: { - Namespaces.rdf: prefixOf(Namespaces.rdf), + builder.namespace(nsRdf, prefixOf(nsRdf)); + builder.element(XmpElements.rdfRoot, namespace: nsRdf, namespaces: { + nsRdf: prefixOf(nsRdf), }); // get element because doc fragment cannot be used to edit root.children.add(builder.buildFragment()); - rdf = root.getElement(rdfRoot, namespace: Namespaces.rdf)!; + rdf = root.getElement(XmpElements.rdfRoot, namespace: nsRdf)!; } // content can be split in multiple `rdf:Description` elements List<XmlNode> descriptions = rdf.children.where((node) { - return node is XmlElement && node.name.local == rdfDescription && node.name.namespaceUri == Namespaces.rdf; + return node is XmlElement && node.name.local == XmpElements.rdfDescription && node.name.namespaceUri == nsRdf; }).toList(); if (descriptions.isEmpty) { final builder = XmlBuilder(); - builder.namespace(Namespaces.rdf, prefixOf(Namespaces.rdf)); - builder.element(rdfDescription, namespace: Namespaces.rdf, attributes: { - '${prefixOf(Namespaces.rdf)}$propNamespaceSeparator$rdfAbout': '', + builder.namespace(nsRdf, prefixOf(nsRdf)); + builder.element(XmpElements.rdfDescription, namespace: nsRdf, attributes: { + '${prefixOf(nsRdf)}$propNamespaceSeparator${XmpAttributes.rdfAbout}': '', }); rdf.children.add(builder.buildFragment()); // get element because doc fragment cannot be used to edit - descriptions.add(rdf.getElement(rdfDescription, namespace: Namespaces.rdf)!); + descriptions.add(rdf.getElement(XmpElements.rdfDescription, namespace: nsRdf)!); } final modified = apply(descriptions); @@ -406,10 +245,10 @@ class XMP { if (rdf.children.isNotEmpty) { if (modified) { - _addNamespaces(descriptions.first, {Namespaces.xmp: prefixOf(Namespaces.xmp)}); + _addNamespaces(descriptions.first, {nsXmp: prefixOf(nsXmp)}); final xmpDate = toXmpDate(modifyDate ?? DateTime.now()); - setAttribute(descriptions, xmpMetadataDate, xmpDate, namespace: Namespaces.xmp, strat: XmpEditStrategy.always); - setAttribute(descriptions, xmpModifyDate, xmpDate, namespace: Namespaces.xmp, strat: XmpEditStrategy.always); + setAttribute(descriptions, XmpAttributes.xmpMetadataDate, xmpDate, namespace: nsXmp, strat: XmpEditStrategy.always); + setAttribute(descriptions, XmpAttributes.xmpModifyDate, xmpDate, namespace: nsXmp, strat: XmpEditStrategy.always); } } else { // clear XMP if there are no attributes or elements worth preserving diff --git a/lib/model/actions/chip_actions.dart b/lib/view/src/actions/chip.dart similarity index 89% rename from lib/model/actions/chip_actions.dart rename to lib/view/src/actions/chip.dart index f71b8c930..cf4b7f0a1 100644 --- a/lib/model/actions/chip_actions.dart +++ b/lib/view/src/actions/chip.dart @@ -1,18 +1,9 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -enum ChipAction { - goToAlbumPage, - goToCountryPage, - goToPlacePage, - goToTagPage, - reverse, - hide, - lockVault, -} - -extension ExtraChipAction on ChipAction { +extension ExtraChipActionView on ChipAction { String getText(BuildContext context) { switch (this) { case ChipAction.goToAlbumPage: diff --git a/lib/model/actions/chip_set_actions.dart b/lib/view/src/actions/chip_set.dart similarity index 73% rename from lib/model/actions/chip_set_actions.dart rename to lib/view/src/actions/chip_set.dart index 7e2efa418..f178505ab 100644 --- a/lib/model/actions/chip_set_actions.dart +++ b/lib/view/src/actions/chip_set.dart @@ -1,74 +1,9 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; -enum ChipSetAction { - // general - configureView, - select, - selectAll, - selectNone, - // browsing - search, - toggleTitleSearch, - createAlbum, - createVault, - // browsing or selecting - map, - slideshow, - stats, - // selecting (single/multiple filters) - delete, - hide, - pin, - unpin, - lockVault, - // selecting (single filter) - rename, - setCover, - configureVault, -} - -class ChipSetActions { - static const general = [ - ChipSetAction.configureView, - ChipSetAction.select, - ChipSetAction.selectAll, - ChipSetAction.selectNone, - ]; - - // `null` items are converted to dividers - static const browsing = [ - ChipSetAction.search, - ChipSetAction.toggleTitleSearch, - null, - ChipSetAction.map, - ChipSetAction.slideshow, - ChipSetAction.stats, - null, - ChipSetAction.createAlbum, - ChipSetAction.createVault, - ]; - - // `null` items are converted to dividers - static const selection = [ - ChipSetAction.setCover, - ChipSetAction.pin, - ChipSetAction.unpin, - ChipSetAction.delete, - ChipSetAction.rename, - ChipSetAction.hide, - null, - ChipSetAction.map, - ChipSetAction.slideshow, - ChipSetAction.stats, - null, - ChipSetAction.configureVault, - ChipSetAction.lockVault, - ]; -} - -extension ExtraChipSetAction on ChipSetAction { +extension ExtraChipSetActionView on ChipSetAction { String getText(BuildContext context) { switch (this) { // general @@ -108,6 +43,8 @@ extension ExtraChipSetAction on ChipSetAction { return context.l10n.chipActionUnpin; case ChipSetAction.lockVault: return context.l10n.chipActionLock; + case ChipSetAction.showCountryStates: + return context.l10n.chipActionShowCountryStates; // selecting (single filter) case ChipSetAction.rename: return context.l10n.chipActionRename; @@ -159,6 +96,8 @@ extension ExtraChipSetAction on ChipSetAction { return AIcons.unpin; case ChipSetAction.lockVault: return AIcons.vaultLock; + case ChipSetAction.showCountryStates: + return AIcons.state; // selecting (single filter) case ChipSetAction.rename: return AIcons.name; diff --git a/lib/model/actions/entry_actions.dart b/lib/view/src/actions/entry.dart similarity index 73% rename from lib/model/actions/entry_actions.dart rename to lib/view/src/actions/entry.dart index 2153f3ac7..960673705 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/view/src/actions/entry.dart @@ -1,145 +1,10 @@ import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -enum EntryAction { - info, - addShortcut, - copyToClipboard, - delete, - restore, - convert, - print, - rename, - copy, - move, - share, - toggleFavourite, - // raster - rotateCCW, - rotateCW, - flip, - // vector - viewSource, - // video - videoCaptureFrame, - videoSelectStreams, - videoSetSpeed, - videoToggleMute, - videoSettings, - videoTogglePlay, - videoReplay10, - videoSkip10, - // external - edit, - open, - openVideo, - openMap, - setAs, - // platform - rotateScreen, - // metadata - editDate, - editLocation, - editTitleDescription, - editRating, - editTags, - removeMetadata, - exportMetadata, - // metadata / GeoTIFF - showGeoTiffOnMap, - // metadata / motion photo - convertMotionPhotoToStillImage, - viewMotionPhotoVideo, - // debug - debug, -} - -class EntryActions { - static const topLevel = [ - EntryAction.info, - EntryAction.share, - EntryAction.edit, - EntryAction.rename, - EntryAction.delete, - EntryAction.copy, - EntryAction.move, - EntryAction.toggleFavourite, - EntryAction.rotateScreen, - EntryAction.viewSource, - ]; - - static const export = [ - ...exportInternal, - ...exportExternal, - ]; - - static const exportInternal = [ - EntryAction.convert, - EntryAction.addShortcut, - EntryAction.copyToClipboard, - EntryAction.print, - ]; - - static const exportExternal = [ - EntryAction.open, - EntryAction.openMap, - EntryAction.setAs, - ]; - - static const pageActions = [ - EntryAction.videoCaptureFrame, - EntryAction.videoSelectStreams, - EntryAction.videoSetSpeed, - EntryAction.videoToggleMute, - EntryAction.videoSettings, - EntryAction.videoTogglePlay, - EntryAction.videoReplay10, - EntryAction.videoSkip10, - EntryAction.rotateCCW, - EntryAction.rotateCW, - EntryAction.flip, - ]; - - static const trashed = [ - EntryAction.delete, - EntryAction.restore, - EntryAction.debug, - ]; - - static const video = [ - EntryAction.videoCaptureFrame, - EntryAction.videoToggleMute, - EntryAction.videoSetSpeed, - EntryAction.videoSelectStreams, - EntryAction.videoSettings, - ]; - - static const videoPlayback = [ - EntryAction.videoReplay10, - EntryAction.videoTogglePlay, - EntryAction.videoSkip10, - ]; - - static const commonMetadataActions = [ - EntryAction.editDate, - EntryAction.editLocation, - EntryAction.editTitleDescription, - EntryAction.editRating, - EntryAction.editTags, - EntryAction.removeMetadata, - EntryAction.exportMetadata, - ]; - - static const formatSpecificMetadataActions = [ - EntryAction.showGeoTiffOnMap, - EntryAction.convertMotionPhotoToStillImage, - EntryAction.viewMotionPhotoVideo, - ]; -} - -extension ExtraEntryAction on EntryAction { +extension ExtraEntryActionView on EntryAction { String getText(BuildContext context) { switch (this) { case EntryAction.info: @@ -178,6 +43,8 @@ extension ExtraEntryAction on EntryAction { case EntryAction.viewSource: return context.l10n.entryActionViewSource; // video + case EntryAction.lockViewer: + return context.l10n.viewerActionLock; case EntryAction.videoCaptureFrame: return context.l10n.videoActionCaptureFrame; case EntryAction.videoToggleMute: @@ -290,6 +157,8 @@ extension ExtraEntryAction on EntryAction { case EntryAction.viewSource: return AIcons.vector; // video + case EntryAction.lockViewer: + return AIcons.viewerLock; case EntryAction.videoCaptureFrame: return AIcons.captureFrame; case EntryAction.videoToggleMute: diff --git a/lib/model/actions/entry_set_actions.dart b/lib/view/src/actions/entry_set.dart similarity index 64% rename from lib/model/actions/entry_set_actions.dart rename to lib/view/src/actions/entry_set.dart index 86984c2f3..880250bc6 100644 --- a/lib/model/actions/entry_set_actions.dart +++ b/lib/view/src/actions/entry_set.dart @@ -1,134 +1,9 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; -enum EntrySetAction { - // general - configureView, - select, - selectAll, - selectNone, - // browsing - searchCollection, - toggleTitleSearch, - addShortcut, - emptyBin, - // browsing or selecting - map, - slideshow, - stats, - rescan, - // selecting - share, - delete, - restore, - copy, - move, - rename, - convert, - toggleFavourite, - rotateCCW, - rotateCW, - flip, - editDate, - editLocation, - editTitleDescription, - editRating, - editTags, - removeMetadata, -} - -class EntrySetActions { - static const general = [ - EntrySetAction.configureView, - EntrySetAction.select, - EntrySetAction.selectAll, - EntrySetAction.selectNone, - ]; - - // `null` items are converted to dividers - static const pageBrowsing = [ - EntrySetAction.searchCollection, - EntrySetAction.toggleTitleSearch, - EntrySetAction.addShortcut, - null, - EntrySetAction.map, - EntrySetAction.slideshow, - EntrySetAction.stats, - null, - EntrySetAction.rescan, - EntrySetAction.emptyBin, - ]; - - // exclude bin related actions - static const collectionEditorBrowsing = [ - EntrySetAction.searchCollection, - EntrySetAction.toggleTitleSearch, - EntrySetAction.addShortcut, - EntrySetAction.map, - EntrySetAction.slideshow, - EntrySetAction.stats, - EntrySetAction.rescan, - ]; - - // `null` items are converted to dividers - static const pageSelection = [ - EntrySetAction.share, - EntrySetAction.delete, - EntrySetAction.restore, - EntrySetAction.copy, - EntrySetAction.move, - EntrySetAction.rename, - EntrySetAction.convert, - EntrySetAction.toggleFavourite, - null, - EntrySetAction.map, - EntrySetAction.slideshow, - EntrySetAction.stats, - null, - EntrySetAction.rescan, - // editing actions are in their subsection - ]; - - // exclude bin related actions - static const collectionEditorSelectionRegular = [ - EntrySetAction.share, - EntrySetAction.delete, - EntrySetAction.copy, - EntrySetAction.move, - EntrySetAction.rename, - EntrySetAction.convert, - EntrySetAction.toggleFavourite, - EntrySetAction.map, - EntrySetAction.slideshow, - EntrySetAction.stats, - EntrySetAction.rescan, - // editing actions are in their subsection - ]; - - static const collectionEditorSelectionEdit = [ - EntrySetAction.rotateCCW, - EntrySetAction.rotateCW, - EntrySetAction.flip, - EntrySetAction.editDate, - EntrySetAction.editLocation, - EntrySetAction.editTitleDescription, - EntrySetAction.editRating, - EntrySetAction.editTags, - EntrySetAction.removeMetadata, - ]; - - static const edit = [ - EntrySetAction.editDate, - EntrySetAction.editLocation, - EntrySetAction.editTitleDescription, - EntrySetAction.editRating, - EntrySetAction.editTags, - EntrySetAction.removeMetadata, - ]; -} - -extension ExtraEntrySetAction on EntrySetAction { +extension ExtraEntrySetActionView on EntrySetAction { String getText(BuildContext context) { switch (this) { // general diff --git a/lib/model/actions/map_actions.dart b/lib/view/src/actions/map.dart similarity index 88% rename from lib/model/actions/map_actions.dart rename to lib/view/src/actions/map.dart index 07cc9189a..05a2ceb71 100644 --- a/lib/model/actions/map_actions.dart +++ b/lib/view/src/actions/map.dart @@ -1,14 +1,9 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -enum MapAction { - selectStyle, - zoomIn, - zoomOut, -} - -extension ExtraMapAction on MapAction { +extension ExtraMapActionView on MapAction { String getText(BuildContext context) { switch (this) { case MapAction.selectStyle: diff --git a/lib/model/actions/map_cluster_actions.dart b/lib/view/src/actions/map_cluster.dart similarity index 85% rename from lib/model/actions/map_cluster_actions.dart rename to lib/view/src/actions/map_cluster.dart index 0f0dd0a63..cc605675f 100644 --- a/lib/model/actions/map_cluster_actions.dart +++ b/lib/view/src/actions/map_cluster.dart @@ -1,13 +1,9 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -enum MapClusterAction { - editLocation, - removeLocation, -} - -extension ExtraMapClusterAction on MapClusterAction { +extension ExtraMapClusterActionView on MapClusterAction { String getText(BuildContext context) { switch (this) { case MapClusterAction.editLocation: diff --git a/lib/model/actions/share_actions.dart b/lib/view/src/actions/share.dart similarity index 87% rename from lib/model/actions/share_actions.dart rename to lib/view/src/actions/share.dart index 74c7bec8a..d9a224a00 100644 --- a/lib/model/actions/share_actions.dart +++ b/lib/view/src/actions/share.dart @@ -1,13 +1,9 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -enum ShareAction { - imageOnly, - videoOnly, -} - -extension ExtraShareAction on ShareAction { +extension ExtraShareActionView on ShareAction { String getText(BuildContext context) { switch (this) { case ShareAction.imageOnly: diff --git a/lib/model/actions/slideshow_actions.dart b/lib/view/src/actions/slideshow.dart similarity index 82% rename from lib/model/actions/slideshow_actions.dart rename to lib/view/src/actions/slideshow.dart index 55d40847f..84c729bad 100644 --- a/lib/model/actions/slideshow_actions.dart +++ b/lib/view/src/actions/slideshow.dart @@ -1,14 +1,9 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/material.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; -enum SlideshowAction { - resume, - showInCollection, - settings, -} - -extension ExtraSlideshowAction on SlideshowAction { +extension ExtraSlideshowActionView on SlideshowAction { String getText(BuildContext context) { switch (this) { case SlideshowAction.resume: diff --git a/lib/model/metadata/enums/date_edit_action.dart b/lib/view/src/metadata/date_edit_action.dart similarity index 87% rename from lib/model/metadata/enums/date_edit_action.dart rename to lib/view/src/metadata/date_edit_action.dart index 4d48b4f0e..de735ff63 100644 --- a/lib/model/metadata/enums/date_edit_action.dart +++ b/lib/view/src/metadata/date_edit_action.dart @@ -1,8 +1,8 @@ -import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -extension ExtraDateEditAction on DateEditAction { +extension ExtraDateEditActionView on DateEditAction { String getText(BuildContext context) { switch (this) { case DateEditAction.setCustom: diff --git a/lib/view/src/metadata/date_field_source.dart b/lib/view/src/metadata/date_field_source.dart new file mode 100644 index 000000000..88355826b --- /dev/null +++ b/lib/view/src/metadata/date_field_source.dart @@ -0,0 +1,20 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraDateFieldSourceView on DateFieldSource { + String getText(BuildContext context) { + switch (this) { + case DateFieldSource.fileModifiedDate: + return context.l10n.editEntryDateDialogSourceFileModifiedDate; + case DateFieldSource.exifDate: + return 'Exif date'; + case DateFieldSource.exifDateOriginal: + return 'Exif original date'; + case DateFieldSource.exifDateDigitized: + return 'Exif digitized date'; + case DateFieldSource.exifGpsDate: + return 'Exif GPS date'; + } + } +} diff --git a/lib/view/src/metadata/fields.dart b/lib/view/src/metadata/fields.dart new file mode 100644 index 000000000..906146443 --- /dev/null +++ b/lib/view/src/metadata/fields.dart @@ -0,0 +1,20 @@ +import 'package:aves_model/aves_model.dart'; + +extension ExtraMetadataFieldView on MetadataField { + String get title { + switch (this) { + case MetadataField.exifDate: + return 'Exif date'; + case MetadataField.exifDateOriginal: + return 'Exif original date'; + case MetadataField.exifDateDigitized: + return 'Exif digitized date'; + case MetadataField.exifGpsDatestamp: + return 'Exif GPS date'; + case MetadataField.xmpXmpCreateDate: + return 'XMP xmp:CreateDate'; + default: + return name; + } + } +} diff --git a/lib/model/metadata/enums/length_unit.dart b/lib/view/src/metadata/length_unit.dart similarity index 77% rename from lib/model/metadata/enums/length_unit.dart rename to lib/view/src/metadata/length_unit.dart index 0da3eb3f0..d4766d902 100644 --- a/lib/model/metadata/enums/length_unit.dart +++ b/lib/view/src/metadata/length_unit.dart @@ -1,8 +1,8 @@ -import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -extension ExtraLengthUnit on LengthUnit { +extension ExtraLengthUnitView on LengthUnit { String getText(BuildContext context) { switch (this) { case LengthUnit.px: diff --git a/lib/model/metadata/enums/location_edit_action.dart b/lib/view/src/metadata/location_edit_action.dart similarity index 83% rename from lib/model/metadata/enums/location_edit_action.dart rename to lib/view/src/metadata/location_edit_action.dart index 5c0f25424..469a15c3d 100644 --- a/lib/model/metadata/enums/location_edit_action.dart +++ b/lib/view/src/metadata/location_edit_action.dart @@ -1,8 +1,8 @@ -import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -extension ExtraLocationEditAction on LocationEditAction { +extension ExtraLocationEditActionView on LocationEditAction { String getText(BuildContext context) { switch (this) { case LocationEditAction.chooseOnMap: diff --git a/lib/view/src/metadata/metadata_type.dart b/lib/view/src/metadata/metadata_type.dart new file mode 100644 index 000000000..5eee2bf45 --- /dev/null +++ b/lib/view/src/metadata/metadata_type.dart @@ -0,0 +1,29 @@ +import 'package:aves_model/aves_model.dart'; + +extension ExtraMetadataTypeView on MetadataType { + // match `metadata-extractor` directory names + String getText() { + switch (this) { + case MetadataType.comment: + return 'Comment'; + case MetadataType.exif: + return 'Exif'; + case MetadataType.iccProfile: + return 'ICC Profile'; + case MetadataType.iptc: + return 'IPTC'; + case MetadataType.jfif: + return 'JFIF'; + case MetadataType.jpegAdobe: + return 'Adobe JPEG'; + case MetadataType.jpegDucky: + return 'Ducky'; + case MetadataType.mp4: + return 'MP4'; + case MetadataType.photoshopIrb: + return 'Photoshop'; + case MetadataType.xmp: + return 'XMP'; + } + } +} diff --git a/lib/model/settings/enums/l10n.dart b/lib/view/src/settings/enums.dart similarity index 77% rename from lib/model/settings/enums/l10n.dart rename to lib/view/src/settings/enums.dart index 88ce874bf..15b1f5031 100644 --- a/lib/model/settings/enums/l10n.dart +++ b/lib/view/src/settings/enums.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -extension ExtraAccessibilityAnimationsName on AccessibilityAnimations { +extension ExtraAccessibilityAnimationsView on AccessibilityAnimations { String getName(BuildContext context) { switch (this) { case AccessibilityAnimations.system: @@ -16,7 +16,7 @@ extension ExtraAccessibilityAnimationsName on AccessibilityAnimations { } } -extension ExtraAccessibilityTimeoutName on AccessibilityTimeout { +extension ExtraAccessibilityTimeoutView on AccessibilityTimeout { String getName(BuildContext context) { switch (this) { case AccessibilityTimeout.system: @@ -35,7 +35,7 @@ extension ExtraAccessibilityTimeoutName on AccessibilityTimeout { } } -extension ExtraAvesThemeBrightnessName on AvesThemeBrightness { +extension ExtraAvesThemeBrightnessView on AvesThemeBrightness { String getName(BuildContext context) { switch (this) { case AvesThemeBrightness.system: @@ -50,7 +50,7 @@ extension ExtraAvesThemeBrightnessName on AvesThemeBrightness { } } -extension ExtraCoordinateFormatName on CoordinateFormat { +extension ExtraCoordinateFormatView on CoordinateFormat { String getName(BuildContext context) { switch (this) { case CoordinateFormat.dms: @@ -61,7 +61,7 @@ extension ExtraCoordinateFormatName on CoordinateFormat { } } -extension ExtraDisplayRefreshRateModeName on DisplayRefreshRateMode { +extension ExtraDisplayRefreshRateModeView on DisplayRefreshRateMode { String getName(BuildContext context) { switch (this) { case DisplayRefreshRateMode.auto: @@ -74,7 +74,7 @@ extension ExtraDisplayRefreshRateModeName on DisplayRefreshRateMode { } } -extension ExtraEntryMapStyleName on EntryMapStyle { +extension ExtraEntryMapStyleView on EntryMapStyle { String getName(BuildContext context) { switch (this) { case EntryMapStyle.googleNormal: @@ -97,7 +97,7 @@ extension ExtraEntryMapStyleName on EntryMapStyle { } } -extension ExtraHomePageSettingName on HomePageSetting { +extension ExtraHomePageSettingView on HomePageSetting { String getName(BuildContext context) { switch (this) { case HomePageSetting.collection: @@ -108,7 +108,7 @@ extension ExtraHomePageSettingName on HomePageSetting { } } -extension ExtraKeepScreenOnName on KeepScreenOn { +extension ExtraKeepScreenOnView on KeepScreenOn { String getName(BuildContext context) { switch (this) { case KeepScreenOn.never: @@ -123,7 +123,7 @@ extension ExtraKeepScreenOnName on KeepScreenOn { } } -extension ExtraSlideshowVideoPlaybackName on SlideshowVideoPlayback { +extension ExtraSlideshowVideoPlaybackView on SlideshowVideoPlayback { String getName(BuildContext context) { switch (this) { case SlideshowVideoPlayback.skip: @@ -136,7 +136,7 @@ extension ExtraSlideshowVideoPlaybackName on SlideshowVideoPlayback { } } -extension ExtraSubtitlePositionName on SubtitlePosition { +extension ExtraSubtitlePositionView on SubtitlePosition { String getName(BuildContext context) { switch (this) { case SubtitlePosition.top: @@ -147,33 +147,7 @@ extension ExtraSubtitlePositionName on SubtitlePosition { } } -extension ExtraThumbnailOverlayLocationIconName on ThumbnailOverlayLocationIcon { - String getName(BuildContext context) { - switch (this) { - case ThumbnailOverlayLocationIcon.located: - return context.l10n.filterLocatedLabel; - case ThumbnailOverlayLocationIcon.unlocated: - return context.l10n.filterNoLocationLabel; - case ThumbnailOverlayLocationIcon.none: - return context.l10n.settingsDisabled; - } - } -} - -extension ExtraThumbnailOverlayTagIconName on ThumbnailOverlayTagIcon { - String getName(BuildContext context) { - switch (this) { - case ThumbnailOverlayTagIcon.tagged: - return context.l10n.filterTaggedLabel; - case ThumbnailOverlayTagIcon.untagged: - return context.l10n.filterNoTagLabel; - case ThumbnailOverlayTagIcon.none: - return context.l10n.settingsDisabled; - } - } -} - -extension ExtraUnitSystemName on UnitSystem { +extension ExtraUnitSystemView on UnitSystem { String getName(BuildContext context) { switch (this) { case UnitSystem.metric: @@ -184,7 +158,7 @@ extension ExtraUnitSystemName on UnitSystem { } } -extension ExtraVideoAutoPlayModeName on VideoAutoPlayMode { +extension ExtraVideoAutoPlayModeView on VideoAutoPlayMode { String getName(BuildContext context) { switch (this) { case VideoAutoPlayMode.disabled: @@ -197,7 +171,7 @@ extension ExtraVideoAutoPlayModeName on VideoAutoPlayMode { } } -extension ExtraVideoBackgroundModeName on VideoBackgroundMode { +extension ExtraVideoBackgroundModeView on VideoBackgroundMode { String getName(BuildContext context) { switch (this) { case VideoBackgroundMode.disabled: @@ -208,7 +182,7 @@ extension ExtraVideoBackgroundModeName on VideoBackgroundMode { } } -extension ExtraVideoControlsName on VideoControls { +extension ExtraVideoControlsView on VideoControls { String getName(BuildContext context) { switch (this) { case VideoControls.play: @@ -223,7 +197,7 @@ extension ExtraVideoControlsName on VideoControls { } } -extension ExtraVideoLoopModeName on VideoLoopMode { +extension ExtraVideoLoopModeView on VideoLoopMode { String getName(BuildContext context) { switch (this) { case VideoLoopMode.never: @@ -236,7 +210,7 @@ extension ExtraVideoLoopModeName on VideoLoopMode { } } -extension ExtraViewerTransitionName on ViewerTransition { +extension ExtraViewerTransitionView on ViewerTransition { String getName(BuildContext context) { switch (this) { case ViewerTransition.slide: @@ -253,7 +227,7 @@ extension ExtraViewerTransitionName on ViewerTransition { } } -extension ExtraWidgetDisplayedItemName on WidgetDisplayedItem { +extension ExtraWidgetDisplayedItemView on WidgetDisplayedItem { String getName(BuildContext context) { switch (this) { case WidgetDisplayedItem.random: @@ -264,7 +238,7 @@ extension ExtraWidgetDisplayedItemName on WidgetDisplayedItem { } } -extension ExtraWidgetOpenPageName on WidgetOpenPage { +extension ExtraWidgetOpenPageView on WidgetOpenPage { String getName(BuildContext context) { switch (this) { case WidgetOpenPage.home: diff --git a/lib/view/src/settings/thumbnail_overlay_location_icon.dart b/lib/view/src/settings/thumbnail_overlay_location_icon.dart new file mode 100644 index 000000000..e479cf91f --- /dev/null +++ b/lib/view/src/settings/thumbnail_overlay_location_icon.dart @@ -0,0 +1,27 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraThumbnailOverlayLocationIconView on ThumbnailOverlayLocationIcon { + String getName(BuildContext context) { + switch (this) { + case ThumbnailOverlayLocationIcon.located: + return context.l10n.filterLocatedLabel; + case ThumbnailOverlayLocationIcon.unlocated: + return context.l10n.filterNoLocationLabel; + case ThumbnailOverlayLocationIcon.none: + return context.l10n.settingsDisabled; + } + } + + IconData getIcon(BuildContext context) { + switch (this) { + case ThumbnailOverlayLocationIcon.unlocated: + return AIcons.locationUnlocated; + case ThumbnailOverlayLocationIcon.located: + case ThumbnailOverlayLocationIcon.none: + return AIcons.location; + } + } +} diff --git a/lib/view/src/settings/thumbnail_overlay_tag_icon.dart b/lib/view/src/settings/thumbnail_overlay_tag_icon.dart new file mode 100644 index 000000000..983d18ffe --- /dev/null +++ b/lib/view/src/settings/thumbnail_overlay_tag_icon.dart @@ -0,0 +1,28 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraThumbnailOverlayTagIconView on ThumbnailOverlayTagIcon { + String getName(BuildContext context) { + switch (this) { + case ThumbnailOverlayTagIcon.tagged: + return context.l10n.filterTaggedLabel; + case ThumbnailOverlayTagIcon.untagged: + return context.l10n.filterNoTagLabel; + case ThumbnailOverlayTagIcon.none: + return context.l10n.settingsDisabled; + } + } + + IconData getIcon(BuildContext context) { + switch (this) { + case ThumbnailOverlayTagIcon.tagged: + return AIcons.tag; + case ThumbnailOverlayTagIcon.untagged: + return AIcons.tagUntagged; + case ThumbnailOverlayTagIcon.none: + return AIcons.tag; + } + } +} diff --git a/lib/view/src/source/album.dart b/lib/view/src/source/album.dart new file mode 100644 index 000000000..5706431dc --- /dev/null +++ b/lib/view/src/source/album.dart @@ -0,0 +1,24 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraAlbumTypeView on AlbumType { + String? getName(BuildContext context) { + switch (this) { + 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.vault: + case AlbumType.app: + return null; + } + } +} diff --git a/lib/view/src/source/group.dart b/lib/view/src/source/group.dart new file mode 100644 index 000000000..851b72292 --- /dev/null +++ b/lib/view/src/source/group.dart @@ -0,0 +1,62 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraEntryGroupFactorView on EntryGroupFactor { + String getName(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case EntryGroupFactor.album: + return l10n.collectionGroupAlbum; + case EntryGroupFactor.month: + return l10n.collectionGroupMonth; + case EntryGroupFactor.day: + return l10n.collectionGroupDay; + case EntryGroupFactor.none: + return l10n.collectionGroupNone; + } + } + + IconData get icon { + switch (this) { + case EntryGroupFactor.album: + return AIcons.album; + case EntryGroupFactor.month: + return AIcons.dateByMonth; + case EntryGroupFactor.day: + return AIcons.dateByDay; + case EntryGroupFactor.none: + return AIcons.clear; + } + } +} + +extension ExtraAlbumChipGroupFactorView on AlbumChipGroupFactor { + String getName(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case AlbumChipGroupFactor.importance: + return l10n.albumGroupTier; + case AlbumChipGroupFactor.mimeType: + return l10n.albumGroupType; + case AlbumChipGroupFactor.volume: + return l10n.albumGroupVolume; + case AlbumChipGroupFactor.none: + return l10n.albumGroupNone; + } + } + + IconData get icon { + switch (this) { + case AlbumChipGroupFactor.importance: + return AIcons.important; + case AlbumChipGroupFactor.mimeType: + return AIcons.mimeType; + case AlbumChipGroupFactor.volume: + return AIcons.removableStorage; + case AlbumChipGroupFactor.none: + return AIcons.clear; + } + } +} diff --git a/lib/view/src/source/layout.dart b/lib/view/src/source/layout.dart new file mode 100644 index 000000000..58d3e8161 --- /dev/null +++ b/lib/view/src/source/layout.dart @@ -0,0 +1,29 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraTileLayoutView on TileLayout { + String getName(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case TileLayout.mosaic: + return l10n.tileLayoutMosaic; + case TileLayout.grid: + return l10n.tileLayoutGrid; + case TileLayout.list: + return l10n.tileLayoutList; + } + } + + IconData get icon { + switch (this) { + case TileLayout.mosaic: + return AIcons.layoutMosaic; + case TileLayout.grid: + return AIcons.layoutGrid; + case TileLayout.list: + return AIcons.layoutList; + } + } +} diff --git a/lib/model/source/enums/view.dart b/lib/view/src/source/sort.dart similarity index 51% rename from lib/model/source/enums/view.dart rename to lib/view/src/source/sort.dart index 7f6c50ffd..705d0238f 100644 --- a/lib/model/source/enums/view.dart +++ b/lib/view/src/source/sort.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -extension ExtraEntrySortFactor on EntrySortFactor { +extension ExtraEntrySortFactorView on EntrySortFactor { String getName(BuildContext context) { final l10n = context.l10n; switch (this) { @@ -46,7 +46,7 @@ extension ExtraEntrySortFactor on EntrySortFactor { } } -extension ExtraChipSortFactor on ChipSortFactor { +extension ExtraChipSortFactorView on ChipSortFactor { String getName(BuildContext context) { final l10n = context.l10n; switch (this) { @@ -87,86 +87,3 @@ extension ExtraChipSortFactor on ChipSortFactor { } } } - -extension ExtraEntryGroupFactor on EntryGroupFactor { - String getName(BuildContext context) { - final l10n = context.l10n; - switch (this) { - case EntryGroupFactor.album: - return l10n.collectionGroupAlbum; - case EntryGroupFactor.month: - return l10n.collectionGroupMonth; - case EntryGroupFactor.day: - return l10n.collectionGroupDay; - case EntryGroupFactor.none: - return l10n.collectionGroupNone; - } - } - - IconData get icon { - switch (this) { - case EntryGroupFactor.album: - return AIcons.album; - case EntryGroupFactor.month: - return AIcons.dateByMonth; - case EntryGroupFactor.day: - return AIcons.dateByDay; - case EntryGroupFactor.none: - return AIcons.clear; - } - } -} - -extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor { - String getName(BuildContext context) { - final l10n = context.l10n; - switch (this) { - case AlbumChipGroupFactor.importance: - return l10n.albumGroupTier; - case AlbumChipGroupFactor.mimeType: - return l10n.albumGroupType; - case AlbumChipGroupFactor.volume: - return l10n.albumGroupVolume; - case AlbumChipGroupFactor.none: - return l10n.albumGroupNone; - } - } - - IconData get icon { - switch (this) { - case AlbumChipGroupFactor.importance: - return AIcons.important; - case AlbumChipGroupFactor.mimeType: - return AIcons.mimeType; - case AlbumChipGroupFactor.volume: - return AIcons.removableStorage; - case AlbumChipGroupFactor.none: - return AIcons.clear; - } - } -} - -extension ExtraTileLayout on TileLayout { - String getName(BuildContext context) { - final l10n = context.l10n; - switch (this) { - case TileLayout.mosaic: - return l10n.tileLayoutMosaic; - case TileLayout.grid: - return l10n.tileLayoutGrid; - case TileLayout.list: - return l10n.tileLayoutList; - } - } - - IconData get icon { - switch (this) { - case TileLayout.mosaic: - return AIcons.layoutMosaic; - case TileLayout.grid: - return AIcons.layoutGrid; - case TileLayout.list: - return AIcons.layoutList; - } - } -} diff --git a/lib/model/source/source_state.dart b/lib/view/src/source/state.dart similarity index 83% rename from lib/model/source/source_state.dart rename to lib/view/src/source/state.dart index 35d57bd7d..7b23db094 100644 --- a/lib/model/source/source_state.dart +++ b/lib/view/src/source/state.dart @@ -1,7 +1,7 @@ import 'package:aves/l10n/l10n.dart'; -import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves_model/aves_model.dart'; -extension ExtraSourceState on SourceState { +extension ExtraSourceStateView on SourceState { String? getName(AppLocalizations l10n) { switch (this) { case SourceState.loading: diff --git a/lib/model/vaults/enums.dart b/lib/view/src/source/vault.dart similarity index 83% rename from lib/model/vaults/enums.dart rename to lib/view/src/source/vault.dart index 27752a438..e72b42ad9 100644 --- a/lib/model/vaults/enums.dart +++ b/lib/view/src/source/vault.dart @@ -1,9 +1,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; -enum VaultLockType { system, pattern, pin, password } - -extension ExtraVaultLockType on VaultLockType { +extension ExtraVaultLockTypeView on VaultLockType { String getText(BuildContext context) { switch (this) { case VaultLockType.system: diff --git a/lib/view/src/storage/relative_dir.dart b/lib/view/src/storage/relative_dir.dart new file mode 100644 index 000000000..23a6efcfd --- /dev/null +++ b/lib/view/src/storage/relative_dir.dart @@ -0,0 +1,12 @@ +import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraVolumeRelativeDirectoryView on VolumeRelativeDirectory { + String getVolumeDescription(BuildContext context) { + final volume = androidFileUtils.storageVolumes.firstWhereOrNull((volume) => volume.path == volumePath); + return volume?.getDescription(context) ?? volumePath; + } +} diff --git a/lib/view/src/storage/volume.dart b/lib/view/src/storage/volume.dart new file mode 100644 index 000000000..4a77b7ff6 --- /dev/null +++ b/lib/view/src/storage/volume.dart @@ -0,0 +1,15 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraStorageVolumeView on StorageVolume { + String getDescription(BuildContext? context) { + if (description != null) return description!; + // ideally, the context should always be provided, but in some cases (e.g. album comparison), + // this would require numerous additional methods to have the context as argument + // for such a minor benefit: fallback volume description on Android < N + final l10n = context?.l10n; + if (isPrimary) return l10n?.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage'; + return l10n?.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card'; + } +} diff --git a/lib/model/wallpaper_target.dart b/lib/view/src/wallpaper_target.dart similarity index 73% rename from lib/model/wallpaper_target.dart rename to lib/view/src/wallpaper_target.dart index 882bfb9b8..597f0bdb4 100644 --- a/lib/model/wallpaper_target.dart +++ b/lib/view/src/wallpaper_target.dart @@ -1,9 +1,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/material.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; -enum WallpaperTarget { home, lock, homeLock } - -extension ExtraWallpaperTarget on WallpaperTarget { +extension ExtraWallpaperTargetView on WallpaperTarget { String getName(BuildContext context) { switch (this) { case WallpaperTarget.home: diff --git a/lib/view/src/xmp.dart b/lib/view/src/xmp.dart new file mode 100644 index 000000000..e2f185726 --- /dev/null +++ b/lib/view/src/xmp.dart @@ -0,0 +1,67 @@ +import 'package:aves/ref/metadata/xmp.dart'; + +class XmpNamespaceView { + // cf https://exiftool.org/TagNames/XMP.html + static const Map<String, String> nsTitles = { + XmpNamespaces.acdsee: 'ACDSee', + XmpNamespaces.adsmlat: 'AdsML', + XmpNamespaces.exifAux: 'Exif Aux', + XmpNamespaces.avm: 'Astronomy Visualization', + XmpNamespaces.camera: 'Pix4D Camera', + XmpNamespaces.cc: 'Creative Commons', + XmpNamespaces.crd: 'Camera Raw Defaults', + XmpNamespaces.creatorAtom: 'After Effects', + XmpNamespaces.crs: 'Camera Raw Settings', + XmpNamespaces.crss: 'Camera Raw Saved Settings', + XmpNamespaces.darktable: 'darktable', + XmpNamespaces.dc: 'Dublin Core', + XmpNamespaces.digiKam: 'digiKam', + XmpNamespaces.droneDji: 'DJI Drone', + XmpNamespaces.dwc: 'Darwin Core', + XmpNamespaces.exif: 'Exif', + XmpNamespaces.exifEx: 'Exif Ex', + XmpNamespaces.fstop: 'F-Stop', + XmpNamespaces.gAudio: 'Google Audio', + XmpNamespaces.gCamera: 'Google Camera', + XmpNamespaces.gContainer: 'Google Container', + XmpNamespaces.gCreations: 'Google Creations', + XmpNamespaces.gDepth: 'Google Depth', + XmpNamespaces.gDevice: 'Google Device', + XmpNamespaces.gFocus: 'Google Focus', + XmpNamespaces.gImage: 'Google Image', + XmpNamespaces.gPano: 'Google Panorama', + XmpNamespaces.gSpherical: 'Google Spherical', + XmpNamespaces.gettyImagesGift: 'Getty Images', + XmpNamespaces.gimp210: 'GIMP 2.10', + XmpNamespaces.gimpXmp: 'GIMP', + XmpNamespaces.illustrator: 'Illustrator', + XmpNamespaces.iptc4xmpCore: 'IPTC Core', + XmpNamespaces.iptc4xmpExt: 'IPTC Extension', + XmpNamespaces.lr: 'Lightroom', + XmpNamespaces.mediapro: 'MediaPro', + XmpNamespaces.miCamera: 'Mi Camera', + XmpNamespaces.microsoftPhoto: 'Microsoft Photo 1.0', + XmpNamespaces.mp1: 'Microsoft Photo 1.1', + XmpNamespaces.mp: 'Microsoft Photo 1.2', + XmpNamespaces.mwgrs: 'Regions', + XmpNamespaces.nga: 'National Gallery of Art', + XmpNamespaces.opMedia: 'OnePlus Media', + XmpNamespaces.panorama: 'Panorama', + XmpNamespaces.panoStudio: 'PanoramaStudio', + XmpNamespaces.pdf: 'PDF', + XmpNamespaces.pdfX: 'PDF/X', + XmpNamespaces.photoMechanic: 'Photo Mechanic', + XmpNamespaces.photoshop: 'Photoshop', + XmpNamespaces.plus: 'PLUS', + XmpNamespaces.pmtm: 'Photomatix', + XmpNamespaces.tiff: 'TIFF', + XmpNamespaces.xmp: 'Basic', + XmpNamespaces.xmpBJ: 'Basic Job Ticket', + XmpNamespaces.xmpDM: 'Dynamic Media', + XmpNamespaces.xmpMM: 'Media Management', + XmpNamespaces.xmpNote: 'Note', + XmpNamespaces.xmpRights: 'Rights Management', + XmpNamespaces.xmpTPg: 'Paged-Text', + XmpNamespaces.xperiaCamera: 'Xperia Camera', + }; +} diff --git a/lib/view/view.dart b/lib/view/view.dart new file mode 100644 index 000000000..76eec0d8c --- /dev/null +++ b/lib/view/view.dart @@ -0,0 +1,27 @@ +export 'src/actions/chip.dart'; +export 'src/actions/chip_set.dart'; +export 'src/actions/entry.dart'; +export 'src/actions/entry_set.dart'; +export 'src/actions/map.dart'; +export 'src/actions/map_cluster.dart'; +export 'src/actions/share.dart'; +export 'src/actions/slideshow.dart'; +export 'src/metadata/date_edit_action.dart'; +export 'src/metadata/date_field_source.dart'; +export 'src/metadata/fields.dart'; +export 'src/metadata/length_unit.dart'; +export 'src/metadata/location_edit_action.dart'; +export 'src/metadata/metadata_type.dart'; +export 'src/settings/enums.dart'; +export 'src/settings/thumbnail_overlay_location_icon.dart'; +export 'src/settings/thumbnail_overlay_tag_icon.dart'; +export 'src/source/album.dart'; +export 'src/source/group.dart'; +export 'src/source/layout.dart'; +export 'src/source/sort.dart'; +export 'src/source/state.dart'; +export 'src/source/vault.dart'; +export 'src/storage/relative_dir.dart'; +export 'src/storage/volume.dart'; +export 'src/wallpaper_target.dart'; +export 'src/xmp.dart'; diff --git a/lib/widget_common.dart b/lib/widget_common.dart index c111a54b5..fe14fee12 100644 --- a/lib/widget_common.dart +++ b/lib/widget_common.dart @@ -3,13 +3,13 @@ import 'dart:async'; import 'package:aves/app_flavor.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/sort.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/media_store_source.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/home_widget.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/widgets/about/about_mobile_page.dart b/lib/widgets/about/about_mobile_page.dart new file mode 100644 index 000000000..995c49bf7 --- /dev/null +++ b/lib/widgets/about/about_mobile_page.dart @@ -0,0 +1,50 @@ +import 'package:aves/widgets/about/app_ref.dart'; +import 'package:aves/widgets/about/bug_report.dart'; +import 'package:aves/widgets/about/credits.dart'; +import 'package:aves/widgets/about/licenses.dart'; +import 'package:aves/widgets/about/translators.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; + +class AboutMobilePage extends StatelessWidget { + const AboutMobilePage({super.key}); + + @override + Widget build(BuildContext context) { + return AvesScaffold( + appBar: AppBar( + title: Text(context.l10n.aboutPageTitle), + ), + body: GestureAreaProtectorStack( + child: SafeArea( + bottom: false, + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.only(top: 16), + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + const AppReference(), + const Divider(), + const BugReport(), + const Divider(), + const AboutCredits(), + const Divider(), + const AboutTranslators(), + const Divider(), + ], + ), + ), + ), + const Licenses(), + const BottomPaddingSliver(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index 357607629..10388dc5f 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -1,18 +1,7 @@ import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/about/app_ref.dart'; -import 'package:aves/widgets/about/bug_report.dart'; -import 'package:aves/widgets/about/credits.dart'; -import 'package:aves/widgets/about/licenses.dart'; -import 'package:aves/widgets/about/translators.dart'; -import 'package:aves/widgets/common/basic/insets.dart'; -import 'package:aves/widgets/common/basic/scaffold.dart'; -import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; -import 'package:aves/widgets/common/behaviour/pop/scope.dart'; -import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/navigation/tv_rail.dart'; +import 'package:aves/widgets/about/about_mobile_page.dart'; +import 'package:aves/widgets/about/about_tv_page.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class AboutPage extends StatelessWidget { static const routeName = '/about'; @@ -21,66 +10,10 @@ class AboutPage extends StatelessWidget { @override Widget build(BuildContext context) { - final appBarTitle = Text(context.l10n.aboutPageTitle); - final useTvLayout = settings.useTvLayout; - final body = CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.only(top: 16), - sliver: SliverList( - delegate: SliverChildListDelegate( - [ - const TvEdgeFocus(), - const AppReference(), - if (!settings.useTvLayout) ...[ - const Divider(), - const BugReport(), - ], - const Divider(), - const AboutCredits(), - const Divider(), - const AboutTranslators(), - const Divider(), - ], - ), - ), - ), - const Licenses(), - const BottomPaddingSliver(), - ], - ); - - if (useTvLayout) { - return AvesScaffold( - body: AvesPopScope( - handlers: const [TvNavigationPopHandler.pop], - child: Row( - children: [ - TvRail( - controller: context.read<TvRailController>(), - ), - Expanded( - child: DirectionalSafeArea( - start: false, - child: body, - ), - ), - ], - ), - ), - ); + if (settings.useTvLayout) { + return const AboutTvPage(); } else { - return AvesScaffold( - appBar: AppBar( - title: appBarTitle, - ), - body: GestureAreaProtectorStack( - child: SafeArea( - bottom: false, - child: body, - ), - ), - ); + return const AboutMobilePage(); } } } diff --git a/lib/widgets/about/about_tv_page.dart b/lib/widgets/about/about_tv_page.dart new file mode 100644 index 000000000..9f52238ac --- /dev/null +++ b/lib/widgets/about/about_tv_page.dart @@ -0,0 +1,223 @@ +import 'package:aves/widgets/about/app_ref.dart'; +import 'package:aves/widgets/about/credits.dart'; +import 'package:aves/widgets/about/translators.dart'; +import 'package:aves/widgets/about/tv_license_page.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; +import 'package:aves/widgets/common/behaviour/pop/scope.dart'; +import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; +import 'package:aves/widgets/navigation/tv_rail.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:provider/provider.dart'; + +class AboutTvPage extends StatelessWidget { + const AboutTvPage({super.key}); + + @override + Widget build(BuildContext context) { + return AvesScaffold( + body: AvesPopScope( + handlers: const [TvNavigationPopHandler.pop], + child: Row( + children: [ + TvRail( + controller: context.read<TvRailController>(), + ), + Expanded( + child: Column( + children: [ + const SizedBox(height: 8), + DirectionalSafeArea( + start: false, + bottom: false, + child: AppBar( + automaticallyImplyLeading: false, + title: Text(context.l10n.aboutPageTitle), + elevation: 0, + primary: false, + ), + ), + Expanded( + child: MediaQuery.removePadding( + context: context, + removeLeft: true, + removeTop: true, + removeRight: true, + removeBottom: true, + child: const _Content(), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _Content extends StatefulWidget { + const _Content({Key? key}) : super(key: key); + + @override + State<_Content> createState() => _ContentState(); +} + +enum _Section { links, credits, translators, licenses } + +class _ContentState extends State<_Content> { + final FocusNode _railFocusNode = FocusNode(); + final ValueNotifier<int> _railIndexNotifier = ValueNotifier(0); + late Future<PackageInfo> _packageInfoLoader; + + static const double railWidth = 256; + + @override + void initState() { + super.initState(); + _packageInfoLoader = PackageInfo.fromPlatform(); + } + + @override + void dispose() { + _railIndexNotifier.dispose(); + _railFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder<int>( + valueListenable: _railIndexNotifier, + builder: (context, selectedIndex, child) { + final rail = Focus( + focusNode: _railFocusNode, + skipTraversal: true, + canRequestFocus: false, + child: ConstrainedBox( + constraints: BoxConstraints.loose(const Size.fromWidth(railWidth)), + child: Center( + child: ListView.builder( + itemBuilder: (context, index) { + final isSelected = index == selectedIndex; + final theme = Theme.of(context); + final colors = theme.colorScheme; + return ListTile( + title: DefaultTextStyle( + style: theme.textTheme.bodyLarge!.copyWith( + color: isSelected ? colors.primary : colors.onSurface.withOpacity(0.64), + ), + child: _getTitle(_Section.values[index]), + ), + selected: isSelected, + contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24), + onTap: () => _railIndexNotifier.value = index, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(123)), + ), + // tileColor: theme.scaffoldBackgroundColor, + ); + }, + itemCount: _Section.values.length, + ), + ), + ), + ); + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + rail, + Expanded( + child: MediaQuery.removePadding( + context: context, + removeLeft: !context.isRtl, + removeRight: context.isRtl, + child: _getBody(_Section.values[selectedIndex]), + ), + ), + ], + ); + }, + ); + } + + Widget _getTitle(_Section key) { + switch (key) { + case _Section.links: + return FutureBuilder<PackageInfo>( + future: _packageInfoLoader, + builder: (context, snapshot) { + return Text('${context.l10n.appName} ${snapshot.data?.version}'); + }, + ); + case _Section.credits: + return Text(context.l10n.aboutCreditsSectionTitle); + case _Section.translators: + return Text(context.l10n.aboutTranslatorsSectionTitle); + case _Section.licenses: + return Text(context.l10n.aboutLicensesSectionTitle); + } + } + + Widget _getBody(_Section key) { + switch (key) { + case _Section.links: + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: AppReference.buildLinks(context) + .map((v) => Padding( + padding: const EdgeInsets.all(16), + child: v, + )) + .toList(), + ); + case _Section.credits: + return Padding( + padding: const EdgeInsets.all(16), + child: AboutCredits.buildBody(context), + ); + case _Section.translators: + return Padding( + padding: const EdgeInsets.all(16), + child: AboutTranslators.buildBody(context), + ); + case _Section.licenses: + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.aboutLicensesBanner), + const SizedBox(height: 16), + Center( + child: AvesOutlinedButton( + label: context.l10n.aboutLicensesShowAllButtonLabel, + onPressed: () => Navigator.maybeOf(context)?.push( + MaterialPageRoute( + builder: (context) { + final theme = Theme.of(context); + final listTileTheme = theme.listTileTheme; + return Theme( + data: theme.copyWith( + listTileTheme: listTileTheme.copyWith( + tileColor: theme.scaffoldBackgroundColor, + ), + ), + child: const TvLicensePage(), + ); + }, + ), + ), + ), + ), + ], + ), + ); + } + } +} diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index 111b97c6b..83c6000d0 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -1,7 +1,6 @@ import 'dart:ui'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/about/policy_page.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -10,10 +9,51 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; class AppReference extends StatefulWidget { + static const avesGithub = 'https://github.com/deckerst/aves'; + const AppReference({super.key}); @override State<AppReference> createState() => _AppReferenceState(); + + static List<Widget> buildLinks(BuildContext context) { + final l10n = context.l10n; + return [ + const LinkChip( + leading: Icon( + AIcons.github, + size: 24, + ), + text: 'GitHub', + urlString: AppReference.avesGithub, + ), + LinkChip( + leading: const Icon( + AIcons.legal, + size: 22, + ), + text: l10n.aboutLinkLicense, + urlString: '${AppReference.avesGithub}/blob/main/LICENSE', + ), + LinkChip( + leading: const Icon( + AIcons.privacy, + size: 22, + ), + text: l10n.aboutLinkPolicy, + onTap: () => _goToPolicyPage(context), + ), + ]; + } + + static void _goToPolicyPage(BuildContext context) { + Navigator.maybeOf(context)?.push( + MaterialPageRoute( + settings: const RouteSettings(name: PolicyPage.routeName), + builder: (context) => const PolicyPage(), + ), + ); + } } class _AppReferenceState extends State<AppReference> { @@ -39,7 +79,12 @@ class _AppReferenceState extends State<AppReference> { children: [ _buildAvesLine(), const SizedBox(height: 16), - _buildLinks(), + Wrap( + alignment: WrapAlignment.center, + spacing: 16, + crossAxisAlignment: WrapCrossAlignment.center, + children: AppReference.buildLinks(context), + ), ], ), ); @@ -65,48 +110,4 @@ class _AppReferenceState extends State<AppReference> { }, ); } - - Widget _buildLinks() { - final l10n = context.l10n; - return Wrap( - alignment: WrapAlignment.center, - spacing: 16, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - const LinkChip( - leading: Icon( - AIcons.github, - size: 24, - ), - text: 'GitHub', - urlString: Constants.avesGithub, - ), - LinkChip( - leading: const Icon( - AIcons.legal, - size: 22, - ), - text: l10n.aboutLinkLicense, - urlString: '${Constants.avesGithub}/blob/main/LICENSE', - ), - LinkChip( - leading: const Icon( - AIcons.privacy, - size: 22, - ), - text: l10n.aboutLinkPolicy, - onTap: _goToPolicyPage, - ), - ], - ); - } - - void _goToPolicyPage() { - Navigator.maybeOf(context)?.push( - MaterialPageRoute( - settings: const RouteSettings(name: PolicyPage.routeName), - builder: (context) => const PolicyPage(), - ), - ); - } } diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 3cd1f7d33..954146ea2 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -4,18 +4,19 @@ import 'dart:io'; import 'package:aves/app_flavor.dart'; import 'package:aves/flutter_version.dart'; import 'package:aves/model/device.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/widgets/about/app_ref.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -34,7 +35,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin { late Future<String> _infoLoader; bool _showInstructions = false; - static const bugReportUrl = '${Constants.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md'; + static const bugReportUrl = '${AppReference.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md'; @override void initState() { @@ -60,7 +61,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), alignment: AlignmentDirectional.centerStart, - child: Text(l10n.aboutBugSectionTitle, style: Constants.knownTitleTextStyle), + child: Text(l10n.aboutBugSectionTitle, style: AStyles.knownTitleText), ), ), body: Padding( @@ -141,24 +142,24 @@ class _BugReportState extends State<BugReport> with FeedbackMixin { } Future<String> _getInfo(BuildContext context) async { + final flavor = context.read<AppFlavor>().toString().split('.')[1]; final packageInfo = await PackageInfo.fromPlatform(); final androidInfo = await DeviceInfoPlugin().androidInfo; - final flavor = context.read<AppFlavor>().toString().split('.')[1]; + final storageVolumes = await storageService.getStorageVolumes(); + final storageGrants = await storageService.getGrantedDirectories(); return [ 'Package: ${packageInfo.packageName}', - 'Aves version: ${packageInfo.version}-$flavor', - 'Aves build: ${packageInfo.buildNumber}', - 'Flutter version: ${version['frameworkVersion']}', - 'Flutter channel: ${version['channel']}', - 'Android version: ${androidInfo.version.release}', - 'Android API: ${androidInfo.version.sdkInt}', + 'Installer: ${packageInfo.installerStore}', + 'Aves version: ${packageInfo.version}-$flavor, build ${packageInfo.buildNumber}', + 'Flutter: ${version['channel']} ${version['frameworkVersion']}', + 'Android version: ${androidInfo.version.release}, API ${androidInfo.version.sdkInt}', 'Android build: ${androidInfo.display}', 'Device: ${androidInfo.manufacturer} ${androidInfo.model}', 'Geocoder: ${device.hasGeocoder ? 'ready' : 'not available'}', 'Mobile services: ${mobileServices.isServiceAvailable ? 'ready' : 'not available'}', 'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}', - 'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}', - 'Installer: ${packageInfo.installerStore}', + 'Storage volumes: ${storageVolumes.map((v) => v.path).join(', ')}', + 'Storage grants: ${storageGrants.join(', ')}', 'Error reporting: ${settings.isErrorReportingAllowed}', ].join('\n'); } diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart index 2fd508712..1aa6e61a4 100644 --- a/lib/widgets/about/credits.dart +++ b/lib/widgets/about/credits.dart @@ -8,33 +8,37 @@ class AboutCredits extends StatelessWidget { @override Widget build(BuildContext context) { - final l10n = context.l10n; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AboutSectionTitle(text: l10n.aboutCreditsSectionTitle), + AboutSectionTitle(text: context.l10n.aboutCreditsSectionTitle), const SizedBox(height: 8), - Text.rich( - TextSpan( - children: [ - TextSpan(text: l10n.aboutCreditsWorldAtlas1), - const WidgetSpan( - child: LinkChip( - text: 'World Atlas', - urlString: 'https://github.com/topojson/world-atlas', - textStyle: TextStyle(fontWeight: FontWeight.bold), - ), - alignment: PlaceholderAlignment.middle, - ), - TextSpan(text: l10n.aboutCreditsWorldAtlas2), - ], - ), - ), + buildBody(context), const SizedBox(height: 8), ], ), ); } + + static Widget buildBody(BuildContext context) { + final l10n = context.l10n; + return Text.rich( + TextSpan( + children: [ + TextSpan(text: l10n.aboutCreditsWorldAtlas1), + const WidgetSpan( + child: LinkChip( + text: 'World Atlas', + urlString: 'https://github.com/topojson/world-atlas', + textStyle: TextStyle(fontWeight: FontWeight.bold), + ), + alignment: PlaceholderAlignment.middle, + ), + TextSpan(text: l10n.aboutCreditsWorldAtlas2), + ], + ), + ); + } } diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index e3d476d58..83982ae7c 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -1,10 +1,8 @@ import 'package:aves/app_flavor.dart'; -import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/app/dependencies.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/theme/colors.dart'; -import 'package:aves/utils/dependencies.dart'; import 'package:aves/widgets/about/title.dart'; -import 'package:aves/widgets/about/tv_license_page.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -52,32 +50,30 @@ class _LicensesState extends State<Licenses> { [ _buildHeader(), const SizedBox(height: 16), - if (!settings.useTvLayout) ...[ - AvesExpansionTile( - title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.android), - expandedNotifier: _expandedNotifier, - children: _platform.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesFlutterPluginsSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _flutterPlugins.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesFlutterPackagesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _flutterPackages.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesDartPackagesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _dartPackages.map((package) => LicenseRow(package: package)).toList(), - ), - ], + AvesExpansionTile( + title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.android), + expandedNotifier: _expandedNotifier, + children: _platform.map((package) => _LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesFlutterPluginsSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _flutterPlugins.map((package) => _LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesFlutterPackagesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _flutterPackages.map((package) => _LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesDartPackagesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _dartPackages.map((package) => _LicenseRow(package: package)).toList(), + ), Center( child: AvesOutlinedButton( label: context.l10n.aboutLicensesShowAllButtonLabel, @@ -85,10 +81,10 @@ class _LicensesState extends State<Licenses> { MaterialPageRoute( builder: (context) => Theme( data: Theme.of(context).copyWith( - // as of Flutter v1.22.4, `cardColor` is used as a background color by `LicensePage` + // as of Flutter v3.7.8, `cardColor` is used as a background color by `LicensePage` cardColor: Theme.of(context).scaffoldBackgroundColor, ), - child: settings.useTvLayout ? const TvLicensePage() : const LicensePage(), + child: const LicensePage(), ), ), ), @@ -116,11 +112,10 @@ class _LicensesState extends State<Licenses> { } } -class LicenseRow extends StatelessWidget { +class _LicenseRow extends StatelessWidget { final Dependency package; - const LicenseRow({ - super.key, + const _LicenseRow({ required this.package, }); diff --git a/lib/widgets/about/title.dart b/lib/widgets/about/title.dart index bfb63b27d..ece76b36b 100644 --- a/lib/widgets/about/title.dart +++ b/lib/widgets/about/title.dart @@ -1,5 +1,5 @@ import 'package:aves/model/settings/settings.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:flutter/material.dart'; class AboutSectionTitle extends StatelessWidget { @@ -15,7 +15,7 @@ class AboutSectionTitle extends StatelessWidget { Widget child = Container( alignment: AlignmentDirectional.centerStart, constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Text(text, style: Constants.knownTitleTextStyle), + child: Text(text, style: AStyles.knownTitleText), ); if (settings.useTvLayout) { diff --git a/lib/widgets/about/translators.dart b/lib/widgets/about/translators.dart index 001668e5a..dba2a6404 100644 --- a/lib/widgets/about/translators.dart +++ b/lib/widgets/about/translators.dart @@ -1,6 +1,7 @@ import 'dart:math'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/model/app/contributors.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/about/title.dart'; import 'package:aves/widgets/common/basic/text/change_highlight.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -10,57 +11,6 @@ import 'package:flutter/material.dart'; class AboutTranslators extends StatelessWidget { const AboutTranslators({super.key}); - static const translators = { - Contributor('D3ZOXY', 'its.ghost.message@gmail.com'), - Contributor('JanWaldhorn', 'weblate@jwh.anonaddy.com'), - Contributor('n-berenice', null), - Contributor('Jonatas de Almeida Barros', 'ajonatas56@gmail.com'), - Contributor('MeFinity', 'me.dot.finity@gmail.com'), - Contributor('Maki', null), - Contributor('HiSubway', 'shenyusoftware@gmail.com'), - Contributor('glemco', 'glemco@posteo.net'), - Contributor('Aerowolf', null), - Contributor('小默', 'duzhe163908@gmail.com'), - Contributor('metezd', 'itoldyouthat@protonmail.com'), - Contributor('Martijn Fabrie', null), - Contributor('Koen Koppens', 'koenkoppens@proton.me'), - Contributor('Emmanouil Papavergis', null), - Contributor('kha84', 'khalukhin@gmail.com'), - Contributor('gallegonovato', 'fran-carro@hotmail.es'), - Contributor('Havokdan', 'havokdan@yahoo.com.br'), - Contributor('Jean Mareilles', 'waged1266@tutanota.com'), - Contributor('이정희', 'daemul72@gmail.com'), - Contributor('Translator-3000', 'weblate.m1d0h@8shield.net'), - Contributor('Ralea Adrian Vicențiu', 'ralea.adrian@gmail.com'), - Contributor('Igor Sorocean', 'sorocean.igor@gmail.com'), - Contributor('JY3', 'GeeyunJY3@gmail.com'), - Contributor('Gediminas Murauskas', 'muziejusinfo@gmail.com'), - Contributor('Oğuz Ersen', 'oguz@ersen.moe'), - Contributor('Allan Nordhøy', 'epost@anotheragency.no'), - Contributor('pemibe', 'pemibe4634@dmonies.com'), - Contributor('Linerly', 'linerly@protonmail.com'), - Contributor('Skrripy', 'rozihrash.ya6w7@simplelogin.com'), - Contributor('vesp', 'vesp@post.cz'), - Contributor('Dan', 'denqwerta@gmail.com'), - Contributor('Tijolinho', 'pedrohenrique29.alfenas@gmail.com'), - Contributor('Piotr K', '1337.kelt@gmail.com'), - Contributor('rehork', 'cooky@e.email'), - Contributor('Eric', 'hamburger2048@users.noreply.hosted.weblate.org'), - Contributor('Aitor Salaberria', 'trslbrr@gmail.com'), - Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'), - Contributor('kaajjo', 'claymanoff@gmail.com'), - Contributor('Eduardo Malaspina', 'vaio0@swismail.com'), - // Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic - // Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic - // Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian - // Contributor('slasb37', 'p84haghi@gmail.com'), // Persian - // Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk - // Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai - // Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew - // Contributor('Martin Frandel', 'martinko.fr@gmail.com'), // Slovak - // Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central) - }; - @override Widget build(BuildContext context) { return Padding( @@ -70,15 +20,19 @@ class AboutTranslators extends StatelessWidget { children: [ AboutSectionTitle(text: context.l10n.aboutTranslatorsSectionTitle), const SizedBox(height: 8), - _RandomTextSpanHighlighter( - spans: translators.map((v) => v.name).toList(), - color: Theme.of(context).colorScheme.onPrimary, - ), + buildBody(context), const SizedBox(height: 16), ], ), ); } + + static Widget buildBody(BuildContext context) { + return _RandomTextSpanHighlighter( + spans: Contributors.translators.map((v) => v.name).toList(), + color: Theme.of(context).colorScheme.onPrimary, + ); + } } class _RandomTextSpanHighlighter extends StatefulWidget { @@ -153,7 +107,7 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> TextSpan( children: [ ...widget.spans.expandIndexed((i, v) => [ - if (i != 0) const TextSpan(text: Constants.separator), + if (i != 0) const TextSpan(text: AText.separator), TextSpan(text: v, style: i == _highlightedIndex ? _animatedStyle.value : _baseStyle), ]) ], @@ -162,10 +116,3 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> ); } } - -class Contributor { - final String name; - final String? weblateEmail; - - const Contributor(this.name, this.weblateEmail); -} diff --git a/lib/widgets/about/tv_license_page.dart b/lib/widgets/about/tv_license_page.dart index abd54b1c8..fa4e2b7e2 100644 --- a/lib/widgets/about/tv_license_page.dart +++ b/lib/widgets/about/tv_license_page.dart @@ -24,6 +24,8 @@ class _TvLicensePageState extends State<TvLicensePage> { final ScrollController _detailsScrollController = ScrollController(); final ValueNotifier<int> _railIndexNotifier = ValueNotifier(0); + static const double railWidth = 256; + final Future<_LicenseData> licenses = LicenseRegistry.licenses .fold<_LicenseData>( _LicenseData(), @@ -65,14 +67,15 @@ class _TvLicensePageState extends State<TvLicensePage> { skipTraversal: true, canRequestFocus: false, child: ConstrainedBox( - constraints: BoxConstraints.loose(const Size.fromWidth(300)), + constraints: BoxConstraints.loose(const Size.fromWidth(railWidth)), child: ListView.builder( itemBuilder: (context, index) { final packageName = packages[index]; final bindings = data.packageLicenseBindings[packageName]!; final isSelected = index == selectedIndex; + final theme = Theme.of(context); return Ink( - color: isSelected ? Theme.of(context).highlightColor : Theme.of(context).cardColor, + color: isSelected ? theme.highlightColor : theme.cardColor, child: ListTile( title: Text(packageName), subtitle: Text(MaterialLocalizations.of(context).licensesPackageDetailText(bindings.length)), diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 85fd93fae..37ee81eba 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -5,12 +5,12 @@ 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/apps.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/screen_on.dart'; import 'package:aves/model/settings/enums/theme_brightness.dart'; import 'package:aves/model/settings/settings.dart'; @@ -18,14 +18,12 @@ 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'; -import 'package:aves_utils/aves_utils.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/theme/themes.dart'; -import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_page.dart'; @@ -40,6 +38,8 @@ import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/navigation/tv_page_transitions.dart'; import 'package:aves/widgets/navigation/tv_rail.dart'; import 'package:aves/widgets/welcome_page.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:equatable/equatable.dart'; import 'package:fijkplayer/fijkplayer.dart'; @@ -57,10 +57,9 @@ class AvesApp extends StatefulWidget { // temporary exclude locales not ready yet for prime time // `ckb`: add `flutter_ckb_localization` and necessary app localization delegates when ready - static final _unsupportedLocales = {'ar', 'ckb', 'fa', 'gl', 'he', 'nn', 'sk', 'th'}.map(Locale.new).toSet(); + static final _unsupportedLocales = {'ar', 'ckb', 'fa', 'gl', 'he', 'hi', 'nn', 'sk', 'th'}.map(Locale.new).toSet(); static final List<Locale> supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList(); static final ValueNotifier<EdgeInsets> cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero); - static final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: 'app-navigator'); // do not monitor all `ModalRoute`s, which would include popup menus, // so that we can react to fullscreen `PageRoute`s only @@ -74,8 +73,7 @@ class AvesApp extends StatefulWidget { @override State<AvesApp> createState() => _AvesAppState(); - static void setSystemUIStyle(BuildContext context) { - final theme = Theme.of(context); + static void setSystemUIStyle(ThemeData theme) { final style = systemUIStyleForBrightness(theme.brightness, theme.scaffoldBackgroundColor); SystemChrome.setSystemUIOverlayStyle(style); } @@ -157,6 +155,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { // - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28 // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0) static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder(); + static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); @override void initState() { @@ -225,12 +224,12 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { final themeBrightness = s.item2; final enableDynamicColor = s.item3; - Constants.updateStylesForLocale(settings.appliedLocale); + AStyles.updateStylesForLocale(settings.appliedLocale); return FutureBuilder<CorePalette?>( future: _dynamicColorPaletteLoader, builder: (context, snapshot) { - const defaultAccent = Themes.defaultAccent; + const defaultAccent = AvesColorsData.defaultAccent; Color lightAccent = defaultAccent, darkAccent = defaultAccent; if (enableDynamicColor) { // `DynamicColorBuilder` from package `dynamic_color` provides light/dark @@ -260,7 +259,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { accessibleNavigation: false, ), child: MaterialApp( - navigatorKey: AvesApp.navigatorKey, + navigatorKey: _navigatorKey, home: home, navigatorObservers: _navigatorObservers, builder: (context, child) => _decorateAppChild( @@ -300,7 +299,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { required Widget? child, }) { if (initialized) { - WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(context)); + WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(Theme.of(context))); } return Selector<Settings, bool>( selector: (context, s) => s.initialized ? s.accessibilityAnimations.animate : true, @@ -494,9 +493,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { void _monitorSettings() { void applyIsInstalledAppAccessAllowed() { if (settings.isInstalledAppAccessAllowed) { - androidFileUtils.initAppNames(); + appInventory.initAppNames(); } else { - androidFileUtils.resetAppNames(); + appInventory.resetAppNames(); } } @@ -511,7 +510,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { void applyForceTvLayout() { _onTvLayoutChanged(); - unawaited(AvesApp.navigatorKey.currentState!.pushAndRemoveUntil( + unawaited(_navigatorKey.currentState!.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: HomePage.routeName), builder: (_) => _getFirstPage(), @@ -573,7 +572,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { } reportService.log('New intent data=$intentData'); - AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( + _navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( settings: const RouteSettings(name: HomePage.routeName), builder: (_) => _getFirstPage(intentData: intentData), )); diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index de01c21f4..f3db7ad22 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/query.dart'; @@ -11,10 +10,9 @@ import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; -import 'package:aves/model/source/enums/view.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; @@ -34,6 +32,7 @@ import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/search/search_delegate.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -144,7 +143,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr } @override - void didChangeMetrics() => _updateStatusBarHeight(); + void didChangeMetrics() { + // when top padding changes + _updateStatusBarHeight(); + // when text scale factor changes + _updateAppBarHeight(); + } @override Widget build(BuildContext context) { @@ -220,7 +224,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr } double get appBarContentHeight { - double height = kToolbarHeight; + final textScaleFactor = context.read<MediaQueryData>().textScaleFactor; + double height = kToolbarHeight * textScaleFactor; if (settings.useTvLayout) { height += CaptionedButton.getTelevisionButtonHeight(context); } @@ -228,7 +233,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr height += FilterBar.preferredHeight; } if (context.read<Query>().enabled) { - height += EntryQueryBar.preferredHeight; + height += EntryQueryBar.getPreferredHeight(textScaleFactor); } return height; } @@ -380,59 +385,57 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr return [ ...quickActionButtons, - MenuIconTheme( - child: PopupMenuButton<EntrySetAction>( - // key is expected by test driver - key: const Key('appbar-menu-button'), - itemBuilder: (context) { - final generalMenuItems = EntrySetActions.general.where(isVisible).map( - (action) => _toMenuItem(action, enabled: canApply(action), selection: selection), - ); + PopupMenuButton<EntrySetAction>( + // key is expected by test driver + key: const Key('appbar-menu-button'), + itemBuilder: (context) { + final generalMenuItems = EntrySetActions.general.where(isVisible).map( + (action) => _toMenuItem(action, enabled: canApply(action), selection: selection), + ); - final browsingMenuActions = EntrySetActions.pageBrowsing.where((v) => !browsingQuickActions.contains(v)); - final selectionMenuActions = EntrySetActions.pageSelection.where((v) => !selectionQuickActions.contains(v)); - final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<EntrySetAction?>[], (prev, v) { - if (v == null && (prev.isEmpty || prev.last == null)) return prev; - return [...prev, v]; - }); - if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) { - contextualMenuActions.removeLast(); - } + final browsingMenuActions = EntrySetActions.pageBrowsing.where((v) => !browsingQuickActions.contains(v)); + final selectionMenuActions = EntrySetActions.pageSelection.where((v) => !selectionQuickActions.contains(v)); + final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<EntrySetAction?>[], (prev, v) { + if (v == null && (prev.isEmpty || prev.last == null)) return prev; + return [...prev, v]; + }); + if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) { + contextualMenuActions.removeLast(); + } - final contextualMenuItems = <PopupMenuEntry<EntrySetAction>>[ - ...contextualMenuActions.map( - (action) { - if (action == null) return const PopupMenuDivider(); - return _toMenuItem(action, enabled: canApply(action), selection: selection); - }, + final contextualMenuItems = <PopupMenuEntry<EntrySetAction>>[ + ...contextualMenuActions.map( + (action) { + if (action == null) return const PopupMenuDivider(); + return _toMenuItem(action, enabled: canApply(action), selection: selection); + }, + ), + if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash) + PopupMenuExpansionPanel<EntrySetAction>( + enabled: hasSelection, + value: 'edit', + icon: AIcons.edit, + title: context.l10n.collectionActionEdit, + items: [ + _buildRotateAndFlipMenuItems(context, canApply: canApply), + ...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)), + ], ), - if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash) - PopupMenuExpansionPanel<EntrySetAction>( - enabled: hasSelection, - value: 'edit', - icon: AIcons.edit, - title: context.l10n.collectionActionEdit, - items: [ - _buildRotateAndFlipMenuItems(context, canApply: canApply), - ...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)), - ], - ), - ]; + ]; - return [ - ...generalMenuItems, - if (contextualMenuItems.isNotEmpty) ...[ - const PopupMenuDivider(), - ...contextualMenuItems, - ], - ]; - }, - onSelected: (action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - await _onActionSelected(action); - }, - ), + return [ + ...generalMenuItems, + if (contextualMenuItems.isNotEmpty) ...[ + const PopupMenuDivider(), + ...contextualMenuItems, + ], + ]; + }, + onSelected: (action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + await _onActionSelected(action); + }, ), ]; } diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index f1d78f0b5..23d4c148a 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; +import 'package:aves/model/app/permissions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/favourite.dart'; @@ -9,12 +10,10 @@ import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/app_bar.dart'; import 'package:aves/widgets/collection/draggable_thumb_label.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart'; @@ -45,6 +44,7 @@ import 'package:aves/widgets/common/thumbnail/notifications.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -299,6 +299,18 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent ScrollController get scrollController => widget.scrollController; + @override + void initState() { + super.initState(); + _appBarHeightNotifier.addListener(_onAppBarHeightChanged); + } + + @override + void dispose() { + _appBarHeightNotifier.removeListener(_onAppBarHeightChanged); + super.dispose(); + } + @override Widget build(BuildContext context) { final scrollView = AnimationLimiter( @@ -339,6 +351,8 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent child: selector, ); } + + void _onAppBarHeightChanged() => setState(() {}); } class _CollectionScaler extends StatelessWidget { @@ -485,7 +499,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge return settings.useTvLayout ? scrollView : _buildDraggableScrollView(scrollView, widget.collection); } - Widget _buildDraggableScrollView(ScrollView scrollView, CollectionLens collection) { + Widget _buildDraggableScrollView(Widget scrollView, CollectionLens collection) { return ValueListenableBuilder<double>( valueListenable: widget.appBarHeightNotifier, builder: (context, appBarHeight, child) { @@ -550,7 +564,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge ); } - ScrollView _buildScrollView(Widget appBar, CollectionLens collection) { + Widget _buildScrollView(Widget appBar, CollectionLens collection) { return CustomScrollView( key: widget.scrollableKey, primary: true, @@ -702,5 +716,5 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge return crumbs; } - Future<bool> get _isStoragePermissionGranted => Future.wait(Constants.storagePermissions.map((v) => v.status)).then((v) => v.any((status) => status.isGranted)); + Future<bool> get _isStoragePermissionGranted => Future.wait(Permissions.storage.map((v) => v.status)).then((v) => v.any((status) => status.isGranted)); } diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart index 6c5bf4bb6..5dc38ace4 100644 --- a/lib/widgets/collection/draggable_thumb_label.dart +++ b/lib/widgets/collection/draggable_thumb_label.dart @@ -2,11 +2,11 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/draggable_thumb_label.dart'; import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 3f690f478..908ad0861 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_set_actions.dart'; -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/favourites.dart'; @@ -14,13 +12,12 @@ import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/naming_pattern.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/model/vaults/vaults.dart'; -import 'package:aves/services/android_app_service.dart'; +import 'package:aves/services/app_service.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; @@ -42,6 +39,7 @@ import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/stats/stats_page.dart'; import 'package:aves/widgets/viewer/slideshow_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -264,7 +262,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware Future<void> _share(BuildContext context) async { final entries = _getTargetItems(context); try { - if (!await androidAppService.shareEntries(entries)) { + if (!await appService.shareEntries(entries)) { await showNoMatchingAppDialog(context); } } on TooManyItemsException catch (_) { @@ -352,7 +350,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } // cleanup - await storageService.deleteEmptyDirectories(storageDirs); + await storageService.deleteEmptyRegularDirectories(storageDirs); }, ); } @@ -417,6 +415,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware Set<String> obsoleteTags = todoItems.expand((entry) => entry.tags).toSet(); Set<String> obsoleteCountryCodes = todoItems.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull().toSet(); + Set<String> obsoleteStateCodes = todoItems.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.stateCode).whereNotNull().toSet(); final dataTypes = <EntryDataType>{}; final source = context.read<CollectionSource>(); @@ -447,6 +446,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (obsoleteCountryCodes.isNotEmpty) { source.invalidateCountryFilterSummary(countryCodes: obsoleteCountryCodes); } + if (obsoleteStateCodes.isNotEmpty) { + source.invalidateStateFilterSummary(stateCodes: obsoleteStateCodes); + } if (obsoleteTags.isNotEmpty) { source.invalidateTagFilterSummary(tags: obsoleteTags); } @@ -633,6 +635,14 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware await _edit(context, entries, (entry) => entry.editTags(newTagsByEntry[entry]!)); } + Future<void> removeTags(BuildContext context, {required Set<AvesEntry> entries, required Set<String> tags}) async { + final newTagsByEntry = Map.fromEntries(entries.map((v) { + return MapEntry(v, v.tags.whereNot(tags.contains).toSet()); + })); + + await _edit(context, entries, (entry) => entry.editTags(newTagsByEntry[entry]!)); + } + Future<void> _removeMetadata(BuildContext context) async { final entries = await _getEditableTargetItems(context, canEdit: (entry) => entry.canRemoveMetadata); if (entries == null || entries.isEmpty) return; @@ -737,7 +747,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final name = result.item2; if (name.isEmpty) return; - await androidAppService.pinToHomeScreen(name, coverEntry, filters: filters); + await appService.pinToHomeScreen(name, coverEntry, filters: filters); if (!device.showPinShortcutFeedback) { showFeedback(context, context.l10n.genericSuccessFeedback); } diff --git a/lib/widgets/collection/grid/headers/album.dart b/lib/widgets/collection/grid/headers/album.dart index dc15e9bfc..d73692831 100644 --- a/lib/widgets/collection/grid/headers/album.dart +++ b/lib/widgets/collection/grid/headers/album.dart @@ -7,6 +7,7 @@ import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/header.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class AlbumSectionHeader extends StatelessWidget { diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index f3333bda7..c99c0c49d 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -3,12 +3,12 @@ import 'dart:math'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/collection/grid/headers/album.dart'; import 'package:aves/widgets/collection/grid/headers/date.dart'; import 'package:aves/widgets/collection/grid/headers/rating.dart'; import 'package:aves/widgets/common/grid/header.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class CollectionSectionHeader extends StatelessWidget { diff --git a/lib/widgets/collection/grid/list_details.dart b/lib/widgets/collection/grid/list_details.dart index 456ffecc1..4d680b81d 100644 --- a/lib/widgets/collection/grid/list_details.dart +++ b/lib/widgets/collection/grid/list_details.dart @@ -4,7 +4,9 @@ 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'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; +import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -58,7 +60,7 @@ class EntryListDetails extends StatelessWidget { children: spans, ), style: style, - strutStyle: Constants.overflowStrutStyle, + strutStyle: AStyles.overflowStrut, softWrap: false, overflow: TextOverflow.fade, ); @@ -78,10 +80,10 @@ class EntryListDetails extends StatelessWidget { final locale = context.l10n.localeName; final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat); final date = entry.bestDate; - final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown; + final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; - final size = entry.sizeBytes; - final sizeText = size != null ? formatFileSize(locale, size) : Constants.overlayUnknown; + final size = entry.burstEntries?.map((v) => v.sizeBytes).sum ?? entry.sizeBytes; + final sizeText = size != null ? formatFileSize(locale, size) : AText.valueNotAvailable; return _buildRow( [ diff --git a/lib/widgets/collection/grid/list_details_theme.dart b/lib/widgets/collection/grid/list_details_theme.dart index adbdcb2a1..e9f666570 100644 --- a/lib/widgets/collection/grid/list_details_theme.dart +++ b/lib/widgets/collection/grid/list_details_theme.dart @@ -1,5 +1,5 @@ import 'package:aves/theme/format.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -43,7 +43,7 @@ class EntryListDetailsTheme extends StatelessWidget { TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle), textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, - strutStyle: Constants.overflowStrutStyle, + strutStyle: AStyles.overflowStrut, )..layout(const BoxConstraints(), parentUsesSize: true)) .getMaxIntrinsicHeight(double.infinity); diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index 38d5bc5b8..da346a880 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -2,13 +2,13 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/services/intent_service.dart'; import 'package:aves/widgets/collection/grid/list_details.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/common/grid/scaling.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/notifications.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/collection/query_bar.dart b/lib/widgets/collection/query_bar.dart index e9bc1cc68..005c7ec02 100644 --- a/lib/widgets/collection/query_bar.dart +++ b/lib/widgets/collection/query_bar.dart @@ -10,8 +10,6 @@ class EntryQueryBar extends StatefulWidget { final ValueNotifier<String> queryNotifier; final FocusNode focusNode; - static const preferredHeight = kToolbarHeight; - const EntryQueryBar({ super.key, required this.queryNotifier, @@ -20,6 +18,8 @@ class EntryQueryBar extends StatefulWidget { @override State<EntryQueryBar> createState() => _EntryQueryBarState(); + + static double getPreferredHeight(double textScaleFactor) => QueryBar.getPreferredHeight(textScaleFactor); } class _EntryQueryBarState extends State<EntryQueryBar> { @@ -52,8 +52,9 @@ class _EntryQueryBarState extends State<EntryQueryBar> { @override Widget build(BuildContext context) { + final textScaleFactor = context.select<MediaQueryData, double>((mq) => mq.textScaleFactor); return Container( - height: EntryQueryBar.preferredHeight, + height: EntryQueryBar.getPreferredHeight(textScaleFactor), alignment: Alignment.topCenter, child: Selector<Selection<AvesEntry>, bool>( selector: (context, selection) => !selection.isSelecting, diff --git a/lib/widgets/common/action_controls/quick_choosers/move_button.dart b/lib/widgets/common/action_controls/quick_choosers/move_button.dart index c6ea6163d..0d5ac1b66 100644 --- a/lib/widgets/common/action_controls/quick_choosers/move_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/move_button.dart @@ -1,13 +1,14 @@ -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/album_chooser.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/action_controls/quick_choosers/rate_button.dart b/lib/widgets/common/action_controls/quick_choosers/rate_button.dart index e14e1cb8e..874e2bf83 100644 --- a/lib/widgets/common/action_controls/quick_choosers/rate_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/rate_button.dart @@ -1,6 +1,7 @@ -import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/rate_chooser.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class RateButton extends ChooserQuickButton<int> { diff --git a/lib/widgets/common/action_controls/quick_choosers/rate_chooser.dart b/lib/widgets/common/action_controls/quick_choosers/rate_chooser.dart index b028f680a..4551e2575 100644 --- a/lib/widgets/common/action_controls/quick_choosers/rate_chooser.dart +++ b/lib/widgets/common/action_controls/quick_choosers/rate_chooser.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/quick_chooser.dart'; import 'package:flutter/material.dart'; @@ -72,7 +73,7 @@ class _RateQuickChooserState extends State<RateQuickChooser> { padding: const EdgeInsets.all(4), child: Icon( _rating < thisRating ? AIcons.rating : AIcons.ratingFull, - color: _rating < thisRating ? Colors.grey : Colors.amber, + color: _rating < thisRating ? AColors.starDisabled : AColors.starEnabled, ), ); }).toList(), diff --git a/lib/widgets/common/action_controls/quick_choosers/share_button.dart b/lib/widgets/common/action_controls/quick_choosers/share_button.dart index 7e0164400..009594358 100644 --- a/lib/widgets/common/action_controls/quick_choosers/share_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/share_button.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/share_chooser.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class ShareButton extends ChooserQuickButton<ShareAction> { diff --git a/lib/widgets/common/action_controls/quick_choosers/share_chooser.dart b/lib/widgets/common/action_controls/quick_choosers/share_chooser.dart index d9d8e6176..c412d2d59 100644 --- a/lib/widgets/common/action_controls/quick_choosers/share_chooser.dart +++ b/lib/widgets/common/action_controls/quick_choosers/share_chooser.dart @@ -1,8 +1,9 @@ import 'dart:async'; -import 'package:aves/model/actions/share_actions.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class ShareQuickChooser extends StatelessWidget { diff --git a/lib/widgets/common/action_controls/quick_choosers/tag_button.dart b/lib/widgets/common/action_controls/quick_choosers/tag_button.dart index 6735d644a..565773837 100644 --- a/lib/widgets/common/action_controls/quick_choosers/tag_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/tag_button.dart @@ -1,13 +1,14 @@ -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/tag_chooser.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart index ff1ae2d70..34248cb63 100644 --- a/lib/widgets/common/action_mixins/entry_editor.dart +++ b/lib/widgets/common/action_mixins/entry_editor.dart @@ -5,7 +5,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/placeholder.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/metadata/date_modifier.dart'; -import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; @@ -17,6 +16,7 @@ import 'package:aves/widgets/dialogs/entry_editors/edit_location_dialog.dart'; import 'package:aves/widgets/dialogs/entry_editors/edit_rating_dialog.dart'; import 'package:aves/widgets/dialogs/entry_editors/remove_metadata_dialog.dart'; import 'package:aves/widgets/dialogs/entry_editors/tag_editor_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; @@ -83,9 +83,7 @@ mixin EntryEditorMixin { if (entries.isEmpty) return null; final filtersByEntry = Map.fromEntries(entries.map((v) { - // use `<CollectionFilter>{...}` instead of `toSet()` to circumvent an implicit typing issue, as of Dart v2.18.2 - final filters = <CollectionFilter>{...v.tags.map(TagFilter.new)}; - return MapEntry(v, filters); + return MapEntry(v, v.tags.map(TagFilter.new).toSet()); })); await Navigator.maybeOf(context)?.push( MaterialPageRoute( diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index 768a2d085..24ecffa80 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -2,7 +2,6 @@ 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/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; @@ -10,8 +9,6 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/metadata/date_modifier.dart'; -import 'package:aves/model/metadata/enums/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/model/source/collection_source.dart'; @@ -21,7 +18,6 @@ import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/media_edit_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; @@ -29,10 +25,11 @@ 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/dialogs/convert_entry_dialog.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -91,14 +88,15 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { unawaited(source.refreshUris(newUris)); final l10n = context.l10n; + // get navigator beforehand because + // local context may be deactivated when action is triggered after navigation + final navigator = Navigator.maybeOf(context); final showAction = isMainMode && newUris.isNotEmpty ? SnackBarAction( label: l10n.showButtonLabel, onPressed: () { - // local context may be deactivated when action is triggered after navigation - final context = AvesApp.navigatorKey.currentContext; - if (context != null) { - Navigator.maybeOf(context)?.pushAndRemoveUntil( + if (navigator != null) { + navigator.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( @@ -172,13 +170,13 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { if (uniqueNames.length < names.length) { final value = await showDialog<NameConflictStrategy>( context: context, - builder: (context) => AvesSelectionDialog<NameConflictStrategy>( + builder: (context) => AvesSingleSelectionDialog<NameConflictStrategy>( 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, ), - routeSettings: const RouteSettings(name: AvesSelectionDialog.routeName), + routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName), ); if (value == null) return; nameConflictStrategy = value; @@ -222,7 +220,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { // cleanup if ({MoveType.move, MoveType.toBin}.contains(moveType)) { - await storageService.deleteEmptyDirectories(originAlbums); + await storageService.deleteEmptyRegularDirectories(originAlbums); } final successCount = successOps.length; @@ -235,17 +233,18 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { SnackBarAction? action; if (count > 0 && appMode == AppMode.main) { + // get navigator beforehand because + // local context may be deactivated when action is triggered after navigation + final navigator = Navigator.maybeOf(context); if (toBin) { if (movedEntries.isNotEmpty) { action = SnackBarAction( // TODO TLAD [l10n] key for "RESTORE" label: l10n.entryActionRestore.toUpperCase(), onPressed: () { - // local context may be deactivated when action is triggered after navigation - final context = AvesApp.navigatorKey.currentContext; - if (context != null) { + if (navigator != null) { doMove( - context, + navigator.context, moveType: MoveType.fromBin, entries: movedEntries, hideShowAction: true, @@ -258,10 +257,8 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { action = SnackBarAction( label: l10n.showButtonLabel, onPressed: () { - // local context may be deactivated when action is triggered after navigation - final context = AvesApp.navigatorKey.currentContext; - if (context != null) { - _showMovedItems(context, destinationAlbums, movedOps); + if (navigator != null) { + _showMovedItems(navigator.context, destinationAlbums, movedOps); } }, ); diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 2ba8fd1c0..ec3ae653d 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -2,9 +2,8 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; -import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/accessibility_timeout.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/services/accessibility_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_mixins/overlay_snack_bar.dart'; @@ -38,7 +37,7 @@ mixin FeedbackMixin { // provide the messenger if feedback happens as the widget is disposed void showFeedbackWithMessenger(BuildContext context, ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) { - _getSnackBarDuration(action != null).then((duration) { + settings.timeToTakeAction.getSnackBarDuration(action != null).then((duration) { final start = DateTime.now(); final theme = Theme.of(context); final snackBarTheme = theme.snackBarTheme; @@ -107,27 +106,6 @@ mixin FeedbackMixin { return horizontalPadding; } - Future<Duration> _getSnackBarDuration(bool hasAction) async { - switch (settings.timeToTakeAction) { - case AccessibilityTimeout.system: - if (hasAction) { - return Duration(milliseconds: await (AccessibilityService.getRecommendedTimeToTakeAction(Durations.opToastActionDisplay))); - } else { - return Duration(milliseconds: await (AccessibilityService.getRecommendedTimeToRead(Durations.opToastTextDisplay))); - } - case AccessibilityTimeout.s1: - return const Duration(seconds: 1); - case AccessibilityTimeout.s3: - return const Duration(seconds: 3); - case AccessibilityTimeout.s5: - return const Duration(seconds: 5); - case AccessibilityTimeout.s10: - return const Duration(seconds: 10); - case AccessibilityTimeout.s30: - return const Duration(seconds: 30); - } - } - // report overlay for multiple operations Future<void> showOpReport<T>({ diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index 5d8fa13f9..680bf013f 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -1,8 +1,11 @@ import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -19,12 +22,12 @@ mixin PermissionAwareMixin { final restrictedInaccessibleDirs = dirs.where(restrictedDirs.contains).toSet(); if (restrictedInaccessibleDirs.isNotEmpty) { - if (entries != null && await storageService.canRequestMediaFileAccess()) { + if (entries != null && await storageService.canRequestMediaFileBulkAccess()) { // request media file access for items in restricted directories final uris = <String>[], mimeTypes = <String>[]; entries.where((entry) { final dir = entry.directory; - return dir != null && restrictedInaccessibleDirs.contains(VolumeRelativeDirectory.fromPath(dir)); + return dir != null && restrictedInaccessibleDirs.contains(androidFileUtils.relativeDirectoryFromPath(dir)); }).forEach((entry) { uris.add(entry.uri); mimeTypes.add(entry.mimeType); @@ -68,17 +71,7 @@ mixin PermissionAwareMixin { // abort if the user cancels in Flutter if (confirmed == null || !confirmed) return false; - if (!await deviceService.isSystemFilePickerEnabled()) { - await showDialog( - context: context, - builder: (context) => AvesDialog( - content: Text(context.l10n.missingSystemFilePickerDialogMessage), - actions: const [OkButton()], - ), - routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), - ); - return false; - } + if (!await _checkSystemFilePickerEnabled(context)) return false; final granted = await storageService.requestDirectoryAccess(dir.dirPath); if (!granted) { @@ -102,4 +95,18 @@ mixin PermissionAwareMixin { routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); } + + Future<bool> _checkSystemFilePickerEnabled(BuildContext context) async { + if (await deviceService.isSystemFilePickerEnabled()) return true; + + await showDialog( + context: context, + builder: (context) => AvesDialog( + content: Text(context.l10n.missingSystemFilePickerDialogMessage), + actions: const [OkButton()], + ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), + ); + return false; + } } diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index d1a93321b..f8998d308 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -1,14 +1,15 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/file_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/action_mixins/vault_aware.dart b/lib/widgets/common/action_mixins/vault_aware.dart index 5c6e8e55c..13b4d9ddd 100644 --- a/lib/widgets/common/action_mixins/vault_aware.dart +++ b/lib/widgets/common/action_mixins/vault_aware.dart @@ -1,13 +1,81 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/vaults/vaults.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/filter_editors/password_dialog.dart'; +import 'package:aves/widgets/dialogs/filter_editors/pattern_dialog.dart'; +import 'package:aves/widgets/dialogs/filter_editors/pin_dialog.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth/error_codes.dart' as auth_error; +import 'package:local_auth/local_auth.dart'; mixin VaultAwareMixin on FeedbackMixin { + Future<bool> _tryUnlock(String dirPath, BuildContext context) async { + if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return true; + + final details = vaults.detailsForPath(dirPath); + if (details == null) return false; + + bool? confirmed; + switch (details.lockType) { + case VaultLockType.system: + try { + confirmed = await LocalAuthentication().authenticate( + localizedReason: context.l10n.authenticateToUnlockVault, + ); + } on PlatformException catch (e, stack) { + if (e.code != 'auth_in_progress') { + // `auth_in_progress`: `Authentication in progress` + await reportService.recordError(e, stack); + } + } + break; + case VaultLockType.pattern: + final pattern = await showDialog<String>( + context: context, + builder: (context) => const PatternDialog(needConfirmation: false), + routeSettings: const RouteSettings(name: PatternDialog.routeName), + ); + if (pattern != null) { + confirmed = pattern == await securityService.readValue(details.passKey); + } + break; + case VaultLockType.pin: + final pin = await showDialog<String>( + context: context, + builder: (context) => const PinDialog(needConfirmation: false), + routeSettings: const RouteSettings(name: PinDialog.routeName), + ); + if (pin != null) { + confirmed = pin == await securityService.readValue(details.passKey); + } + break; + case VaultLockType.password: + final password = await showDialog<String>( + context: context, + builder: (context) => const PasswordDialog(needConfirmation: false), + routeSettings: const RouteSettings(name: PasswordDialog.routeName), + ); + if (password != null) { + confirmed = password == await securityService.readValue(details.passKey); + } + break; + } + + if (confirmed == null || !confirmed) return false; + + vaults.unlock(dirPath); + return true; + } + Future<bool> unlockAlbum(BuildContext context, String dirPath) async { - final success = await vaults.tryUnlock(dirPath, context); + final success = await _tryUnlock(dirPath, context); if (!success) { showFeedback(context, context.l10n.genericFailureFeedback); } @@ -29,4 +97,60 @@ mixin VaultAwareMixin on FeedbackMixin { } void lockFilters(Set<AlbumFilter> filters) => vaults.lock(filters.map((v) => v.album).toSet()); + + Future<bool> setVaultPass(BuildContext context, VaultDetails details) async { + switch (details.lockType) { + case VaultLockType.system: + final l10n = context.l10n; + try { + return await LocalAuthentication().authenticate( + localizedReason: l10n.authenticateToConfigureVault, + ); + } on PlatformException catch (e, stack) { + await showDialog( + context: context, + builder: (context) => AvesDialog( + content: Text(e.message ?? l10n.genericFailureFeedback), + actions: const [OkButton()], + ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), + ); + if (e.code != auth_error.notAvailable) { + await reportService.recordError(e, stack); + } + } + break; + case VaultLockType.pattern: + final pattern = await showDialog<String>( + context: context, + builder: (context) => const PatternDialog(needConfirmation: true), + routeSettings: const RouteSettings(name: PatternDialog.routeName), + ); + if (pattern != null) { + return await securityService.writeValue(details.passKey, pattern); + } + break; + case VaultLockType.pin: + final pin = await showDialog<String>( + context: context, + builder: (context) => const PinDialog(needConfirmation: true), + routeSettings: const RouteSettings(name: PinDialog.routeName), + ); + if (pin != null) { + return await securityService.writeValue(details.passKey, pin); + } + break; + case VaultLockType.password: + final password = await showDialog<String>( + context: context, + builder: (context) => const PasswordDialog(needConfirmation: true), + routeSettings: const RouteSettings(name: PasswordDialog.routeName), + ); + if (password != null) { + return await securityService.writeValue(details.passKey, password); + } + break; + } + return false; + } } diff --git a/lib/widgets/common/app_bar/app_bar_subtitle.dart b/lib/widgets/common/app_bar/app_bar_subtitle.dart index a021b149c..b0a5c751a 100644 --- a/lib/widgets/common/app_bar/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar/app_bar_subtitle.dart @@ -1,11 +1,11 @@ import 'dart:ui'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/events.dart'; -import 'package:aves/model/source/source_state.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class SourceStateAwareAppBarTitle extends StatelessWidget { @@ -83,6 +83,9 @@ class SourceStateSubtitle extends StatelessWidget { ] ], ), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, ); }, ), diff --git a/lib/widgets/common/app_bar/app_bar_title.dart b/lib/widgets/common/app_bar/app_bar_title.dart index 09a7897a4..19283c9d9 100644 --- a/lib/widgets/common/app_bar/app_bar_title.dart +++ b/lib/widgets/common/app_bar/app_bar_title.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class InteractiveAppBarTitle extends StatelessWidget { final GestureTapCallback? onTap; @@ -19,7 +20,7 @@ class InteractiveAppBarTitle extends StatelessWidget { child: Container( alignment: AlignmentDirectional.centerStart, color: Colors.transparent, - height: kToolbarHeight, + height: kToolbarHeight * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor), child: child, ), ); diff --git a/lib/widgets/common/basic/color_indicator.dart b/lib/widgets/common/basic/color_indicator.dart new file mode 100644 index 000000000..34f31bcd4 --- /dev/null +++ b/lib/widgets/common/basic/color_indicator.dart @@ -0,0 +1,30 @@ +import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:flutter/widgets.dart'; + +class ColorIndicator extends StatelessWidget { + final Color? value; + final Widget? child; + + static const double radius = 16; + + const ColorIndicator({ + super.key, + required this.value, + this.child, + }); + + @override + Widget build(BuildContext context) { + const dimension = radius * 2; + return Container( + height: dimension, + width: dimension, + decoration: BoxDecoration( + color: value, + border: AvesBorder.border(context), + shape: BoxShape.circle, + ), + child: child, + ); + } +} diff --git a/lib/widgets/common/basic/draggable_scrollbar/scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar/scrollbar.dart index ee6fdb0f2..9e4bf83c9 100644 --- a/lib/widgets/common/basic/draggable_scrollbar/scrollbar.dart +++ b/lib/widgets/common/basic/draggable_scrollbar/scrollbar.dart @@ -62,9 +62,9 @@ class DraggableScrollbar extends StatefulWidget { final double Function(double scrollOffset, double offsetIncrement)? dragOffsetSnapper; /// The view that will be scrolled with the scroll thumb - final ScrollView child; + final Widget child; - DraggableScrollbar({ + const DraggableScrollbar({ super.key, required this.backgroundColor, required this.scrollThumbSize, @@ -78,7 +78,7 @@ class DraggableScrollbar extends StatefulWidget { required this.labelTextBuilder, required this.crumbTextBuilder, required this.child, - }) : assert(child.scrollDirection == Axis.vertical); + }); @override State<DraggableScrollbar> createState() => _DraggableScrollbarState(); diff --git a/lib/widgets/common/basic/font_size_icon_theme.dart b/lib/widgets/common/basic/font_size_icon_theme.dart new file mode 100644 index 000000000..ea43d4449 --- /dev/null +++ b/lib/widgets/common/basic/font_size_icon_theme.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; + +// scale icons according to text scale +class FontSizeIconTheme extends StatelessWidget { + final Widget child; + + const FontSizeIconTheme({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final iconTheme = IconTheme.of(context); + return IconTheme( + data: iconTheme.copyWith( + size: iconTheme.size! * MediaQuery.textScaleFactorOf(context), + ), + child: child, + ); + } +} diff --git a/lib/widgets/common/basic/list_tiles/color.dart b/lib/widgets/common/basic/list_tiles/color.dart index b33196a67..93d2e2ade 100644 --- a/lib/widgets/common/basic/list_tiles/color.dart +++ b/lib/widgets/common/basic/list_tiles/color.dart @@ -1,9 +1,8 @@ import 'package:aves/model/settings/settings.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/basic/color_indicator.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; -import 'package:flex_color_picker/flex_color_picker.dart'; +import 'package:flex_color_picker/flex_color_picker.dart' show ColorPicker, ColorPickerType; import 'package:flutter/material.dart'; class ColorListTile extends StatelessWidget { @@ -11,8 +10,6 @@ class ColorListTile extends StatelessWidget { final Color value; final ValueSetter<Color> onChanged; - static const radius = Constants.colorPickerRadius; - const ColorListTile({ super.key, required this.title, @@ -24,16 +21,10 @@ class ColorListTile extends StatelessWidget { Widget build(BuildContext context) { return ListTile( title: Text(title), - trailing: Container( - height: radius * 2, - width: radius * 2, - decoration: BoxDecoration( - color: value, - border: AvesBorder.border(context), - shape: BoxShape.circle, - ), + trailing: ColorIndicator( + value: value, ), - contentPadding: const EdgeInsetsDirectional.only(start: 16, end: 36 - radius), + contentPadding: const EdgeInsetsDirectional.only(start: 16, end: 36 - ColorIndicator.radius), onTap: () async { final color = await showDialog<Color>( context: context, diff --git a/lib/widgets/common/basic/popup/container.dart b/lib/widgets/common/basic/popup/container.dart index eaa767536..30a0323c3 100644 --- a/lib/widgets/common/basic/popup/container.dart +++ b/lib/widgets/common/basic/popup/container.dart @@ -16,10 +16,10 @@ class PopupMenuItemContainer<T> extends PopupMenuEntry<T> { bool represents(void value) => false; @override - State<PopupMenuItemContainer> createState() => _TransitionPopupMenuItemState(); + State<PopupMenuItemContainer> createState() => _PopupMenuItemContainerState(); } -class _TransitionPopupMenuItemState extends State<PopupMenuItemContainer> { +class _PopupMenuItemContainerState extends State<PopupMenuItemContainer> { @override Widget build(BuildContext context) { return TooltipTheme( diff --git a/lib/widgets/common/basic/popup/menu_row.dart b/lib/widgets/common/basic/popup/menu_row.dart index 28442c841..33773c04f 100644 --- a/lib/widgets/common/basic/popup/menu_row.dart +++ b/lib/widgets/common/basic/popup/menu_row.dart @@ -32,24 +32,3 @@ class MenuRow extends StatelessWidget { ); } } - -// scale icons according to text scale -class MenuIconTheme extends StatelessWidget { - final Widget child; - - const MenuIconTheme({ - super.key, - required this.child, - }); - - @override - Widget build(BuildContext context) { - final iconTheme = IconTheme.of(context); - return IconTheme( - data: iconTheme.copyWith( - size: iconTheme.size! * MediaQuery.textScaleFactorOf(context), - ), - child: child, - ); - } -} diff --git a/lib/widgets/common/basic/query_bar.dart b/lib/widgets/common/basic/query_bar.dart index 4eb44b912..2c75c93aa 100644 --- a/lib/widgets/common/basic/query_bar.dart +++ b/lib/widgets/common/basic/query_bar.dart @@ -1,6 +1,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; @@ -24,6 +25,8 @@ class QueryBar extends StatefulWidget { @override State<QueryBar> createState() => _QueryBarState(); + + static double getPreferredHeight(double textScaleFactor) => kToolbarHeight * textScaleFactor; } class _QueryBarState extends State<QueryBar> { @@ -53,45 +56,50 @@ class _QueryBarState extends State<QueryBar> { return DefaultTextStyle( style: Theme.of(context).textTheme.bodyMedium!, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextField( - controller: _controller, - focusNode: widget.focusNode, - decoration: InputDecoration( - icon: Padding( - padding: widget.leadingPadding ?? const EdgeInsetsDirectional.only(start: 16), - child: Icon(widget.icon ?? AIcons.filter), - ), - hintText: widget.hintText ?? MaterialLocalizations.of(context).searchFieldLabel, - hintStyle: Theme.of(context).inputDecorationTheme.hintStyle, - ), - textInputAction: TextInputAction.search, - onChanged: (s) => _debouncer(() => queryNotifier.value = s.trim()), - enabled: widget.editable, - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 16), - child: ValueListenableBuilder<TextEditingValue>( - valueListenable: _controller, - builder: (context, value, child) => AnimatedSwitcher( - duration: Durations.appBarActionChangeAnimation, - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - axis: Axis.horizontal, - sizeFactor: animation, - child: child, + child: FontSizeIconTheme( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextField( + controller: _controller, + focusNode: widget.focusNode, + decoration: InputDecoration( + icon: Padding( + padding: widget.leadingPadding ?? const EdgeInsetsDirectional.only(start: 16), + // set theme at this level because `InputDecoration` defines its own `IconTheme` with a fixed size + child: FontSizeIconTheme( + child: Icon(widget.icon ?? AIcons.filter), + ), ), + hintText: widget.hintText ?? MaterialLocalizations.of(context).searchFieldLabel, + hintStyle: Theme.of(context).inputDecorationTheme.hintStyle, ), - child: value.text.isNotEmpty ? clearButton : const SizedBox(), + textInputAction: TextInputAction.search, + onChanged: (s) => _debouncer(() => queryNotifier.value = s.trim()), + enabled: widget.editable, ), ), - ), - ], + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 16), + child: ValueListenableBuilder<TextEditingValue>( + valueListenable: _controller, + builder: (context, value, child) => AnimatedSwitcher( + duration: Durations.appBarActionChangeAnimation, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + axis: Axis.horizontal, + sizeFactor: animation, + child: child, + ), + ), + child: value.text.isNotEmpty ? clearButton : const SizedBox(), + ), + ), + ), + ], + ), ), ); } diff --git a/lib/widgets/common/basic/wheel.dart b/lib/widgets/common/basic/wheel.dart index 0434a3c12..b6f190380 100644 --- a/lib/widgets/common/basic/wheel.dart +++ b/lib/widgets/common/basic/wheel.dart @@ -25,8 +25,6 @@ class _WheelSelectorState<T> extends State<WheelSelector<T>> { late final FixedExtentScrollController _controller; final ValueNotifier<bool> _focusedNotifier = ValueNotifier(false); - static const itemSize = Size(40, 40); - ValueNotifier<T> get valueNotifier => widget.valueNotifier; List<T> get values => widget.values; @@ -51,6 +49,7 @@ class _WheelSelectorState<T> extends State<WheelSelector<T>> { const background = Colors.transparent; final foreground = DefaultTextStyle.of(context).style.color!; final transitionDuration = context.select<DurationsData, Duration>((v) => v.formTransition); + final itemSize = Size.square(40 * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor)); return FocusableActionDetector( shortcuts: const { diff --git a/lib/widgets/common/behaviour/pop/tv_navigation.dart b/lib/widgets/common/behaviour/pop/tv_navigation.dart index 67d091e0b..8af4081ff 100644 --- a/lib/widgets/common/behaviour/pop/tv_navigation.dart +++ b/lib/widgets/common/behaviour/pop/tv_navigation.dart @@ -1,4 +1,3 @@ -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'; @@ -6,6 +5,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/expandable_filter_row.dart b/lib/widgets/common/expandable_filter_row.dart index 3557e651c..a3dccd4ce 100644 --- a/lib/widgets/common/expandable_filter_row.dart +++ b/lib/widgets/common/expandable_filter_row.dart @@ -2,7 +2,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; @@ -34,7 +34,7 @@ class TitledExpandableFilterRow extends StatelessWidget { Widget header = Text( title, - style: Constants.knownTitleTextStyle, + style: AStyles.knownTitleText, ); void toggle() => expandedNotifier.value = isExpanded ? null : title; if (settings.useTvLayout) { @@ -49,6 +49,8 @@ class TitledExpandableFilterRow extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ header, + const SizedBox(width: 16), + Icon(isExpanded ? AIcons.collapse : AIcons.expand), ], ), ), diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index d6da38773..16be78103 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -3,7 +3,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:flutter/material.dart'; @@ -32,27 +32,13 @@ class SectionHeader<T> extends StatelessWidget { @override Widget build(BuildContext context) { - Widget child = _buildContent(context); - if (settings.useTvLayout) { - child = InkWell( - onTap: _onTap(context), - borderRadius: const BorderRadius.all(Radius.circular(123)), - child: child, - ); - } - return Container( - alignment: AlignmentDirectional.centerStart, - margin: margin, - child: child, - ); - } + final onTap = selectable ? () => _toggleSectionSelection(context) : null; - Widget _buildContent(BuildContext context) { - return Container( + Widget child = Container( padding: padding, constraints: BoxConstraints(minHeight: leadingSize.height), child: GestureDetector( - onTap: _onTap(context), + onTap: onTap, onLongPress: selectable ? () { final selection = context.read<Selection<T>>(); @@ -80,12 +66,12 @@ class SectionHeader<T> extends StatelessWidget { child: leading, ) : null, - onPressed: _onTap(context), + onPressed: onTap, ), ), TextSpan( text: title, - style: Constants.unknownTitleTextStyle, + style: AStyles.unknownTitleText, ), if (trailing != null) WidgetSpan( @@ -100,10 +86,24 @@ class SectionHeader<T> extends StatelessWidget { ), ), ); + if (settings.useTvLayout) { + // prevent ink response when tapping the header does nothing, + // because otherwise Play Store reviewers think it is broken navigation + child = context.select<Selection<T>, bool>((v) => v.isSelecting) + ? InkWell( + onTap: onTap, + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: child, + ) + : Focus(child: child); + } + return Container( + alignment: AlignmentDirectional.centerStart, + margin: margin, + child: child, + ); } - VoidCallback? _onTap(BuildContext context) => selectable ? () => _toggleSectionSelection(context) : null; - List<T> _getSectionEntries(BuildContext context) => context.read<SectionedListLayout<T>>().sections[sectionKey] ?? []; void _toggleSectionSelection(BuildContext context) { @@ -130,7 +130,7 @@ class SectionHeader<T> extends StatelessWidget { final para = RenderParagraph( TextSpan( children: [ - // as of Flutter v2.8.1, `RenderParagraph` fails to lay out `WidgetSpan` offscreen + // as of Flutter v3.7.7, `RenderParagraph` fails to lay out `WidgetSpan` offscreen // so we use a hair space times a magic number to match width TextSpan( // 23 hair spaces match a width of 40.0 @@ -141,7 +141,7 @@ class SectionHeader<T> extends StatelessWidget { if (hasTrailing) TextSpan(text: '\u200A' * 17), TextSpan( text: title, - style: Constants.unknownTitleTextStyle, + style: AStyles.unknownTitleText, ), ], ), diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index 86cf83777..9d657887b 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index ec7c55972..767b86cf9 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -1,11 +1,11 @@ import 'package:aves/model/highlight.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart'; import 'package:aves/widgets/common/grid/sections/fixed/scale_overlay.dart'; import 'package:aves/widgets/common/grid/sections/mosaic/scale_overlay.dart'; import 'package:aves/widgets/common/grid/sections/section_layout_builder.dart'; import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/grid/sections/fixed/scale_grid.dart b/lib/widgets/common/grid/sections/fixed/scale_grid.dart index 9f9d869fd..dc94c5ddc 100644 --- a/lib/widgets/common/grid/sections/fixed/scale_grid.dart +++ b/lib/widgets/common/grid/sections/fixed/scale_grid.dart @@ -1,6 +1,6 @@ import 'dart:ui' as ui; -import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class FixedExtentGridPainter extends CustomPainter { diff --git a/lib/widgets/common/grid/sections/fixed/scale_overlay.dart b/lib/widgets/common/grid/sections/fixed/scale_overlay.dart index 4bae5d50e..93b018aee 100644 --- a/lib/widgets/common/grid/sections/fixed/scale_overlay.dart +++ b/lib/widgets/common/grid/sections/fixed/scale_overlay.dart @@ -1,8 +1,8 @@ -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/utils/colors.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -123,12 +123,12 @@ class _OverlayBackgroundState extends State<_OverlayBackground> { gradient: RadialGradient( colors: isDark ? const [ - Constants.transparentBlack, - Constants.transparentBlack, + ColorUtils.transparentBlack, + ColorUtils.transparentBlack, ] : const [ - Constants.transparentWhite, - Constants.transparentWhite, + ColorUtils.transparentWhite, + ColorUtils.transparentWhite, ], ), ); diff --git a/lib/widgets/common/grid/sections/fixed/section_layout.dart b/lib/widgets/common/grid/sections/fixed/section_layout.dart index 5b38d8caf..7cdfbc96b 100644 --- a/lib/widgets/common/grid/sections/fixed/section_layout.dart +++ b/lib/widgets/common/grid/sections/fixed/section_layout.dart @@ -28,14 +28,14 @@ class FixedExtentSectionLayout extends SectionLayout { @override int getMinChildIndexForScrollOffset(double scrollOffset) { scrollOffset -= bodyMinOffset; - if (scrollOffset < 0 || mainAxisStride == 0) return firstIndex; + if (mainAxisStride == 0 || !scrollOffset.isFinite || scrollOffset < 0) return firstIndex; return bodyFirstIndex + scrollOffset ~/ mainAxisStride; } @override int getMaxChildIndexForScrollOffset(double scrollOffset) { scrollOffset -= bodyMinOffset; - if (scrollOffset < 0 || mainAxisStride == 0) return firstIndex; + if (mainAxisStride == 0 || !scrollOffset.isFinite || scrollOffset < 0) return firstIndex; return bodyFirstIndex + (scrollOffset / mainAxisStride).ceil() - 1; } } diff --git a/lib/widgets/common/grid/sections/mosaic/scale_overlay.dart b/lib/widgets/common/grid/sections/mosaic/scale_overlay.dart index 07e7ed81d..ea6298e42 100644 --- a/lib/widgets/common/grid/sections/mosaic/scale_overlay.dart +++ b/lib/widgets/common/grid/sections/mosaic/scale_overlay.dart @@ -1,5 +1,5 @@ import 'package:aves/theme/durations.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/utils/colors.dart'; import 'package:aves/widgets/common/grid/sections/mosaic/scale_grid.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:flutter/material.dart'; @@ -110,7 +110,7 @@ class _OverlayBackgroundState extends State<_OverlayBackground> { color: isDark ? Colors.black87 : const Color(0xDDFFFFFF), ) : BoxDecoration( - color: isDark ? Constants.transparentBlack : Constants.transparentWhite, + color: isDark ? ColorUtils.transparentBlack : ColorUtils.transparentWhite, ); } } diff --git a/lib/widgets/common/grid/sections/provider.dart b/lib/widgets/common/grid/sections/provider.dart index 1cc6cf077..d8b2eb964 100644 --- a/lib/widgets/common/grid/sections/provider.dart +++ b/lib/widgets/common/grid/sections/provider.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/common/grid/sections/fixed/section_layout_builder.dart'; import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:aves/widgets/common/grid/sections/mosaic/section_layout_builder.dart'; import 'package:aves/widgets/common/grid/sections/section_layout_builder.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/grid/sections/section_layout_builder.dart b/lib/widgets/common/grid/sections/section_layout_builder.dart index 6b7570d8f..27866181d 100644 --- a/lib/widgets/common/grid/sections/section_layout_builder.dart +++ b/lib/widgets/common/grid/sections/section_layout_builder.dart @@ -1,8 +1,8 @@ -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:aves/widgets/common/grid/sections/section_layout.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart index 72aabace6..a7258cfa3 100644 --- a/lib/widgets/common/grid/theme.dart +++ b/lib/widgets/common/grid/theme.dart @@ -4,9 +4,9 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/favourites.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/identity/aves_app_bar.dart b/lib/widgets/common/identity/aves_app_bar.dart index 7ad74c6f4..3d5b57559 100644 --- a/lib/widgets/common/identity/aves_app_bar.dart +++ b/lib/widgets/common/identity/aves_app_bar.dart @@ -1,6 +1,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/aves_app.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:flutter/material.dart'; @@ -55,7 +56,7 @@ class AvesAppBar extends StatelessWidget { child: Column( children: [ SizedBox( - height: kToolbarHeight, + height: kToolbarHeight * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor), child: Row( children: [ leading != null @@ -65,7 +66,9 @@ class AvesAppBar extends StatelessWidget { tag: leadingHeroTag, flightShuttleBuilder: _flightShuttleBuilder, transitionOnUserGestures: true, - child: leading!, + child: FontSizeIconTheme( + child: leading!, + ), ), ) : const SizedBox(width: 16), @@ -78,12 +81,14 @@ class AvesAppBar extends StatelessWidget { transitionOnUserGestures: true, child: AnimatedSwitcher( duration: context.read<DurationsData>().iconAnimation, - child: Row( - key: ValueKey(transitionKey), - children: [ - Expanded(child: title), - ...actions, - ], + child: FontSizeIconTheme( + child: Row( + key: ValueKey(transitionKey), + children: [ + Expanded(child: title), + ...actions, + ], + ), ), ), ), diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 49062e520..87809c610 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/covers.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; @@ -13,11 +12,14 @@ 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/view/view.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; @@ -127,7 +129,7 @@ class AvesFilterChip extends StatefulWidget { } return PopupMenuItem( value: action, - child: MenuIconTheme( + child: FontSizeIconTheme( child: MenuRow(text: text, icon: action.getIcon()), ), ); diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index 0428f0ca0..022389b7d 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -5,9 +5,9 @@ import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/vaults/vaults.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:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/identity/aves_logo.dart b/lib/widgets/common/identity/aves_logo.dart index 19a4b161c..c6c03d914 100644 --- a/lib/widgets/common/identity/aves_logo.dart +++ b/lib/widgets/common/identity/aves_logo.dart @@ -1,7 +1,7 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/colors.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/identity/highlight_title.dart b/lib/widgets/common/identity/highlight_title.dart index 657d27106..44984ffa9 100644 --- a/lib/widgets/common/identity/highlight_title.dart +++ b/lib/widgets/common/identity/highlight_title.dart @@ -1,10 +1,10 @@ import 'dart:ui'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; import 'package:aves/widgets/common/fx/highlight_decoration.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index 9d72ae6e7..ecb325cd9 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -1,13 +1,14 @@ -import 'package:aves/model/actions/map_actions.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/map/buttons/button.dart'; import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart'; import 'package:aves/widgets/common/map/compass.dart'; import 'package:aves/widgets/common/map/map_action_delegate.dart'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 0a2e8691e..5d008fb20 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -6,14 +6,14 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/sort.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/ref/poi.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/utils/math_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/common/map/attribution.dart'; @@ -21,7 +21,8 @@ import 'package:aves/widgets/common/map/buttons/panel.dart'; import 'package:aves/widgets/common/map/decorator.dart'; import 'package:aves/widgets/common/map/leaflet/map.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/common.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; import 'package:aves_map/aves_map.dart'; import 'package:aves_utils/aves_utils.dart'; import 'package:collection/collection.dart'; @@ -212,7 +213,7 @@ class _GeoMapState extends State<GeoMap> { child: OverlayTextButton( onPressed: () => showSelectionDialog<EntryMapStyle>( context: context, - builder: (context) => AvesSelectionDialog<EntryMapStyle?>( + builder: (context) => AvesSingleSelectionDialog<EntryMapStyle?>( initialValue: settings.mapStyle, options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))), title: context.l10n.mapStyleDialogTitle, @@ -330,7 +331,7 @@ class _GeoMapState extends State<GeoMap> { // fallback to default center var center = settings.mapDefaultCenter; if (center == null) { - center = Constants.wonders[Random().nextInt(Constants.wonders.length)]; + center = PointsOfInterest.wonders[Random().nextInt(PointsOfInterest.wonders.length)]; WidgetsBinding.instance.addPostFrameCallback((_) => settings.mapDefaultCenter = center); } bounds = ZoomedBounds.fromPoints( diff --git a/lib/widgets/common/map/leaflet/scale_layer.dart b/lib/widgets/common/map/leaflet/scale_layer.dart index d305941e1..af99267d9 100644 --- a/lib/widgets/common/map/leaflet/scale_layer.dart +++ b/lib/widgets/common/map/leaflet/scale_layer.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/widgets/common/map/map_action_delegate.dart b/lib/widgets/common/map/map_action_delegate.dart index a41c840d5..e108d01bb 100644 --- a/lib/widgets/common/map/map_action_delegate.dart +++ b/lib/widgets/common/map/map_action_delegate.dart @@ -1,10 +1,11 @@ -import 'package:aves/model/actions/map_actions.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/common.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -18,7 +19,7 @@ class MapActionDelegate { case MapAction.selectStyle: showSelectionDialog<EntryMapStyle>( context: context, - builder: (context) => AvesSelectionDialog<EntryMapStyle?>( + builder: (context) => AvesSingleSelectionDialog<EntryMapStyle?>( initialValue: settings.mapStyle, options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))), title: context.l10n.mapStyleDialogTitle, diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index a639f475f..0c3464951 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -7,7 +7,6 @@ import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/model/entry/extensions/props.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/basic/insets.dart'; @@ -15,6 +14,7 @@ import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/common/fx/transition_image.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/thumbnail/error.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart index af96119e2..b7f42041c 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/android_apps.dart @@ -1,6 +1,6 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/apps.dart'; import 'package:aves/services/common/services.dart'; -import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/common.dart'; @@ -23,7 +23,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au @override void initState() { super.initState(); - _loader = androidAppService.getPackages(); + _loader = appService.getPackages(); } @override diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 51d102076..ca723e04e 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/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/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; @@ -49,7 +50,7 @@ class _AppDebugPageState extends State<AppDebugPage> { appBar: AppBar( title: const Text('Debug'), actions: [ - MenuIconTheme( + FontSizeIconTheme( child: PopupMenuButton<AppDebugAction>( // key is expected by test driver key: const Key('appbar-menu-button'), diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index 8ea4ba4dc..7e242fd05 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -1,6 +1,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/dialogs/aves_confirmation_dialog.dart b/lib/widgets/dialogs/aves_confirmation_dialog.dart index 09cfa1c2d..ffbba695d 100644 --- a/lib/widgets/dialogs/aves_confirmation_dialog.dart +++ b/lib/widgets/dialogs/aves_confirmation_dialog.dart @@ -1,6 +1,6 @@ -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:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'aves_dialog.dart'; diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart deleted file mode 100644 index 068ef5dd6..000000000 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; - -import 'aves_dialog.dart'; - -Future<void> showSelectionDialog<T>({ - required BuildContext context, - required WidgetBuilder builder, - required void Function(T value) onSelection, -}) async { - final value = await showDialog<T>( - context: context, - builder: builder, - routeSettings: const RouteSettings(name: AvesSelectionDialog.routeName), - ); - // wait for the dialog to hide as applying the change may block the UI - await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); - if (value != null) { - onSelection(value); - } -} - -typedef TextBuilder<T> = String Function(T value); - -class AvesSelectionDialog<T> extends StatefulWidget { - static const routeName = '/dialog/selection'; - - final T initialValue; - final Map<T, String> options; - final TextBuilder<T>? optionSubtitleBuilder; - final String? title, message, confirmationButtonLabel; - final bool? dense; - - const AvesSelectionDialog({ - super.key, - required this.initialValue, - required this.options, - this.optionSubtitleBuilder, - this.title, - this.message, - this.confirmationButtonLabel, - this.dense, - }); - - @override - State<AvesSelectionDialog<T>> createState() => _AvesSelectionDialogState<T>(); -} - -class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> { - late T _selectedValue; - - @override - void initState() { - super.initState(); - _selectedValue = widget.initialValue; - } - - @override - Widget build(BuildContext context) { - final title = widget.title; - final message = widget.message; - final verticalPadding = (title == null && message == null) ? AvesDialog.cornerRadius.y / 2 : .0; - final confirmationButtonLabel = widget.confirmationButtonLabel; - final needConfirmation = confirmationButtonLabel != null; - return AvesDialog( - title: title, - scrollableContent: [ - if (verticalPadding != 0) SizedBox(height: verticalPadding), - if (message != null) - Padding( - padding: const EdgeInsets.all(16), - child: Text(message), - ), - ...widget.options.entries.map((kv) { - final radioValue = kv.key; - final radioTitle = kv.value; - return SelectionRadioListTile( - value: radioValue, - title: radioTitle, - optionSubtitleBuilder: widget.optionSubtitleBuilder, - needConfirmation: needConfirmation, - dense: widget.dense, - getGroupValue: () => _selectedValue, - setGroupValue: (v) => setState(() => _selectedValue = v), - ); - }), - if (verticalPadding != 0) SizedBox(height: verticalPadding), - ], - actions: [ - const CancelButton(), - if (needConfirmation) - TextButton( - onPressed: () => Navigator.maybeOf(context)?.pop(_selectedValue), - child: Text(confirmationButtonLabel), - ), - ], - ); - } -} - -class SelectionRadioListTile<T> extends StatelessWidget { - final T value; - final String title; - final TextBuilder<T>? optionSubtitleBuilder; - final bool needConfirmation; - final bool? dense; - final T Function() getGroupValue; - final void Function(T value) setGroupValue; - - const SelectionRadioListTile({ - super.key, - required this.value, - required this.title, - this.optionSubtitleBuilder, - required this.needConfirmation, - this.dense, - required this.getGroupValue, - required this.setGroupValue, - }); - - @override - Widget build(BuildContext context) { - final subtitle = optionSubtitleBuilder?.call(value); - return ReselectableRadioListTile<T>( - // key is expected by test driver - key: Key('$value'), - value: value, - groupValue: getGroupValue(), - onChanged: (v) { - if (needConfirmation) { - setGroupValue(v as T); - } else { - Navigator.maybeOf(context)?.pop(v); - } - }, - reselectable: true, - title: Text( - title, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - subtitle: subtitle != null - ? Text( - subtitle, - softWrap: false, - overflow: TextOverflow.fade, - ) - : null, - dense: dense, - ); - } -} diff --git a/lib/widgets/dialogs/convert_entry_dialog.dart b/lib/widgets/dialogs/convert_entry_dialog.dart index 090c1b0f4..5d4235758 100644 --- a/lib/widgets/dialogs/convert_entry_dialog.dart +++ b/lib/widgets/dialogs/convert_entry_dialog.dart @@ -1,16 +1,17 @@ +import 'package:aves/model/app/support.dart'; import 'package:aves/model/entry/entry.dart'; -import 'package:aves/model/entry/extensions/props.dart'; -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/enums/length_unit.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/media/media_edit_service.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/utils/mime_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/transitions.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -149,7 +150,7 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> { ), ), const SizedBox(width: 8), - const Text(ExtraAvesEntryProps.resolutionSeparator), + const Text(AText.resolutionSeparator), const SizedBox(width: 8), Expanded( child: TextField( @@ -205,7 +206,7 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> { valueListenable: _mimeTypeNotifier, builder: (context, mimeType, child) { Widget child; - if (MimeTypes.canEditExif(mimeType) || MimeTypes.canEditIptc(mimeType) || MimeTypes.canEditXmp(mimeType)) { + if (AppSupport.canEditExif(mimeType) || AppSupport.canEditIptc(mimeType) || AppSupport.canEditXmp(mimeType)) { child = SwitchListTile( value: _writeMetadata, onChanged: (v) => setState(() => _writeMetadata = v), diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index de368dc55..f46d2c53c 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -1,15 +1,12 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/metadata/date_modifier.dart'; -import 'package:aves/model/metadata/enums/date_edit_action.dart'; -import 'package:aves/model/metadata/enums/date_field_source.dart'; -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/fields.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/utils/time_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/basic/wheel.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -18,6 +15,7 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/item_picker.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart index f5e6a8e34..4096bd154 100644 --- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart @@ -1,15 +1,14 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/metadata_edition.dart'; -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/enums/location_edit_action.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/ref/poi.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/transitions.dart'; @@ -18,6 +17,7 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/item_picker.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; @@ -250,7 +250,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> { controller: _latitudeController, decoration: InputDecoration( labelText: l10n.editEntryLocationDialogLatitude, - hintText: coordinateFormatter.format(Constants.pointNemo.latitude), + hintText: coordinateFormatter.format(PointsOfInterest.pointNemo.latitude), ), onChanged: (_) => _validate(), ), @@ -258,7 +258,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> { controller: _longitudeController, decoration: InputDecoration( labelText: l10n.editEntryLocationDialogLongitude, - hintText: coordinateFormatter.format(Constants.pointNemo.longitude), + hintText: coordinateFormatter.format(PointsOfInterest.pointNemo.longitude), ), onChanged: (_) => _validate(), ), diff --git a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart index c118cbb8f..8004d5e1c 100644 --- a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart @@ -1,4 +1,5 @@ import 'package:aves/model/entry/entry.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -73,7 +74,7 @@ class _EditEntryRatingDialogState extends State<EditEntryRatingDialog> { padding: const EdgeInsets.all(4), child: Icon( _rating < thisRating ? AIcons.rating : AIcons.ratingFull, - color: _rating < thisRating ? Colors.grey : Colors.amber, + color: _rating < thisRating ? AColors.starDisabled : AColors.starEnabled, ), ), ); diff --git a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart index a2636158d..b31de3de8 100644 --- a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart @@ -1,15 +1,14 @@ -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/enums/metadata_type.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/highlight_decoration.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart index ff1a175a8..c524323bb 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart @@ -2,9 +2,9 @@ import 'dart:io'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/common/services.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class RenameEntryDialog extends StatefulWidget { @@ -42,21 +42,30 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> { @override Widget build(BuildContext context) { - final isRtl = context.isRtl; - final extensionSuffixText = '${Constants.fsi}${entry.extension}${Constants.pdi}'; return AvesDialog( - content: TextField( - controller: _nameController, - decoration: InputDecoration( - labelText: context.l10n.renameEntryDialogLabel, - // decoration prefix and suffix follow directionality - // but the file extension should always be on the right - prefixText: isRtl ? extensionSuffixText : null, - suffixText: isRtl ? null : extensionSuffixText, - ), - autofocus: true, - onChanged: (_) => _validate(), - onSubmitted: (_) => _submit(context), + content: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: context.l10n.renameEntryDialogLabel, + ), + autofocus: true, + maxLines: null, + onChanged: (_) => _validate(), + onSubmitted: (_) => _submit(context), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 4, bottom: 12), + child: Text( + '${Unicode.FSI}${entry.extension}${Unicode.PDI}', + style: TextStyle(color: Theme.of(context).hintColor), + ), + ), + ], ), actions: [ const CancelButton(), @@ -78,7 +87,7 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> { return pContext.join(entry.directory!, '$name${entry.extension}'); } - String get newName => _nameController.text.trimLeft(); + String get newName => _nameController.text.trimLeft().replaceAll('\n', ''); Future<void> _validate() async { final _newName = newName; diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart index 55d3f68d7..3ba035090 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart @@ -5,8 +5,9 @@ import 'package:aves/model/naming_pattern.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -81,7 +82,7 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> { autofocus: true, ), ), - MenuIconTheme( + FontSizeIconTheme( child: PopupMenuButton<String>( itemBuilder: (context) { return [ @@ -116,7 +117,7 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> { padding: const EdgeInsets.all(16), child: Text( l10n.renameEntrySetPagePreviewSectionTitle, - style: Constants.knownTitleTextStyle, + style: AStyles.knownTitleText, ), ), Expanded( diff --git a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart index 87d5e66a1..1fc090ca2 100644 --- a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart +++ b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart @@ -32,10 +32,14 @@ class _TagEditorPageState extends State<TagEditorPage> { final FocusNode _newTagTextFocusNode = FocusNode(); final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null); late final List<CollectionFilter> _topTags; - late final List<PlaceholderFilter> _placeholders = [PlaceholderFilter.country, PlaceholderFilter.place]; final List<CollectionFilter> _userAddedFilters = []; static const Color untaggedColor = Colors.blueGrey; + static final List<PlaceholderFilter> _placeholders = [ + PlaceholderFilter.country, + PlaceholderFilter.state, + PlaceholderFilter.place, + ]; Map<AvesEntry, Set<CollectionFilter>> get tagsByEntry => widget.filtersByEntry; diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index 30aee7b04..dbd30f69d 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -4,12 +4,11 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.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/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/basic/color_indicator.dart'; import 'package:aves/widgets/common/basic/list_tiles/color.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; @@ -17,6 +16,7 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/item_picker.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/app_pick_page.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; @@ -56,7 +56,6 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> { static const double itemPickerExtent = 46; static const double appPickerExtent = 32; - static const double colorPickerRadius = Constants.colorPickerRadius; double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context)); @@ -323,14 +322,8 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> { if (_customColor != null) GestureDetector( onTap: _pickColor, - child: Container( - height: colorPickerRadius * 2, - width: colorPickerRadius * 2, - decoration: BoxDecoration( - color: _customColor, - border: AvesBorder.border(context), - shape: BoxShape.circle, - ), + child: ColorIndicator( + value: _customColor, ), ), ], diff --git a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart index def2ecef2..c31895d9a 100644 --- a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart @@ -3,8 +3,10 @@ import 'dart:io'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/dialogs/filter_editors/edit_vault_dialog.dart b/lib/widgets/dialogs/filter_editors/edit_vault_dialog.dart index 165a8c055..19cc23aa1 100644 --- a/lib/widgets/dialogs/filter_editors/edit_vault_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/edit_vault_dialog.dart @@ -3,13 +3,17 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/vaults/details.dart'; -import 'package:aves/model/vaults/enums.dart'; import 'package:aves/model/vaults/vaults.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_caption.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/dialogs/selection_dialogs/common.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -27,7 +31,7 @@ class EditVaultDialog extends StatefulWidget { State<EditVaultDialog> createState() => _EditVaultDialogState(); } -class _EditVaultDialogState extends State<EditVaultDialog> { +class _EditVaultDialogState extends State<EditVaultDialog> with FeedbackMixin, VaultAwareMixin { final TextEditingController _nameController = TextEditingController(); late bool _useBin; late bool _autoLockScreenOff; @@ -105,7 +109,7 @@ class _EditVaultDialogState extends State<EditVaultDialog> { _unfocus(); showSelectionDialog<VaultLockType>( context: context, - builder: (context) => AvesSelectionDialog<VaultLockType>( + builder: (context) => AvesSingleSelectionDialog<VaultLockType>( initialValue: _lockType, options: Map.fromEntries(_lockTypeOptions.map((v) => MapEntry(v, v.getText(context)))), ), @@ -178,7 +182,7 @@ class _EditVaultDialogState extends State<EditVaultDialog> { useBin: _useBin, lockType: _lockType, ); - if (!await vaults.setPass(context, details)) return; + if (!await setVaultPass(context, details)) return; Navigator.maybeOf(context)?.pop(details); } diff --git a/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart index b36a59bcb..6a74cd192 100644 --- a/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart @@ -1,19 +1,15 @@ import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/chip_set_actions.dart'; -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/selection.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/basic/popup/menu_row.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; @@ -26,6 +22,7 @@ import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart'; import 'package:aves/widgets/filter_grids/common/app_bar.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; @@ -209,29 +206,29 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { }) { return [ if (widget.moveType != null) - ..._quickActions.where(isVisible).map((action) => IconButton( - icon: action.getIcon(), - onPressed: () => onActionSelected(action), - tooltip: action.getText(context), - )), - MenuIconTheme( - child: PopupMenuButton<ChipSetAction>( - itemBuilder: (context) { - return _menuActions.where((v) => v == null || isVisible(v)).map((action) { - if (action == null) return const PopupMenuDivider(); - return FilterGridAppBar.toMenuItem(context, action, enabled: true); - }).toList(); - }, - onSelected: (action) async { - // remove focus, if any, to prevent the keyboard from showing up - // after the user is done with the popup menu - FocusManager.instance.primaryFocus?.unfocus(); + ..._quickActions.where(isVisible).map( + (action) => IconButton( + icon: action.getIcon(), + onPressed: () => onActionSelected(action), + tooltip: action.getText(context), + ), + ), + PopupMenuButton<ChipSetAction>( + itemBuilder: (context) { + return _menuActions.where((v) => v == null || isVisible(v)).map((action) { + if (action == null) return const PopupMenuDivider(); + return FilterGridAppBar.toMenuItem(context, action, enabled: true); + }).toList(); + }, + onSelected: (action) async { + // remove focus, if any, to prevent the keyboard from showing up + // after the user is done with the popup menu + FocusManager.instance.primaryFocus?.unfocus(); - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - onActionSelected(action); - }, - ), + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + onActionSelected(action); + }, ), ]; } diff --git a/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart index 2b9799064..997388968 100644 --- a/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart @@ -1,7 +1,7 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/apps.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; -import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; @@ -34,7 +34,7 @@ class _AppPickPageState extends State<AppPickPage> { void initState() { super.initState(); _selectedValue = widget.initialValue; - _loader = androidAppService.getPackages(); + _loader = appService.getPackages(); } @override diff --git a/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart index a14a15fad..a20f76ad4 100644 --- a/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart @@ -8,7 +8,8 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/geocoding_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -168,7 +169,6 @@ class _LocationInfo extends StatelessWidget { final ValueNotifier<LatLng?> locationNotifier; static const double iconPadding = 8.0; - static const double iconSize = 16.0; static const double _interRowPadding = 2.0; const _LocationInfo({ @@ -217,6 +217,8 @@ class _LocationInfo extends StatelessWidget { }, ); } + + static double getIconSize(BuildContext context) => 16.0 * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor); } class _AddressRow extends StatefulWidget { @@ -253,7 +255,7 @@ class _AddressRowState extends State<_AddressRow> { mainAxisSize: MainAxisSize.min, children: [ const SizedBox(width: _LocationInfo.iconPadding), - const Icon(AIcons.location, size: _LocationInfo.iconSize), + Icon(AIcons.location, size: _LocationInfo.getIconSize(context)), const SizedBox(width: _LocationInfo.iconPadding), Expanded( child: Container( @@ -266,8 +268,8 @@ class _AddressRowState extends State<_AddressRow> { valueListenable: _addressLineNotifier, builder: (context, addressLine, child) { return Text( - addressLine ?? Constants.overlayUnknown, - strutStyle: Constants.overflowStrutStyle, + addressLine ?? AText.valueNotAvailable, + strutStyle: AStyles.overflowStrut, softWrap: false, overflow: TextOverflow.fade, maxLines: 1, @@ -312,11 +314,16 @@ class _CoordinateRow extends StatelessWidget { return Row( children: [ const SizedBox(width: _LocationInfo.iconPadding), - const Icon(AIcons.geoBounds, size: _LocationInfo.iconSize), + Icon(AIcons.geoBounds, size: _LocationInfo.getIconSize(context)), const SizedBox(width: _LocationInfo.iconPadding), - Text( - location != null ? settings.coordinateFormat.format(context.l10n, location!) : Constants.overlayUnknown, - strutStyle: Constants.overflowStrutStyle, + Expanded( + child: Text( + location != null ? settings.coordinateFormat.format(context.l10n, location!) : AText.valueNotAvailable, + strutStyle: AStyles.overflowStrut, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), ), ], ); diff --git a/lib/widgets/dialogs/selection_dialogs/common.dart b/lib/widgets/dialogs/selection_dialogs/common.dart new file mode 100644 index 000000000..4a827e56f --- /dev/null +++ b/lib/widgets/dialogs/selection_dialogs/common.dart @@ -0,0 +1,23 @@ +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +Future<void> showSelectionDialog<T>({ + required BuildContext context, + required WidgetBuilder builder, + required void Function(T value) onSelection, +}) async { + final value = await showDialog<T>( + context: context, + builder: builder, + routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName), + ); + // wait for the dialog to hide as applying the change may block the UI + await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); + if (value != null) { + onSelection(value); + } +} + +typedef TextBuilder<T> = String Function(T value); diff --git a/lib/widgets/dialogs/selection_dialogs/multi_selection.dart b/lib/widgets/dialogs/selection_dialogs/multi_selection.dart new file mode 100644 index 000000000..f31b7e364 --- /dev/null +++ b/lib/widgets/dialogs/selection_dialogs/multi_selection.dart @@ -0,0 +1,91 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/common.dart'; +import 'package:flutter/material.dart'; + +class AvesMultiSelectionDialog<T> extends StatefulWidget { + static const routeName = '/dialog/multi_selection'; + + final Set<T> initialValue; + final Map<T, String> options; + final TextBuilder<T>? optionSubtitleBuilder; + final String? title, message; + final bool? dense; + + const AvesMultiSelectionDialog({ + super.key, + required this.initialValue, + required this.options, + this.optionSubtitleBuilder, + this.title, + this.message, + this.dense, + }); + + @override + State<AvesMultiSelectionDialog<T>> createState() => _AvesMultiSelectionDialogState<T>(); +} + +class _AvesMultiSelectionDialogState<T> extends State<AvesMultiSelectionDialog<T>> { + late Set<T> _selectedValues; + + @override + void initState() { + super.initState(); + _selectedValues = widget.initialValue; + } + + @override + Widget build(BuildContext context) { + final title = widget.title; + final message = widget.message; + final verticalPadding = (title == null && message == null) ? AvesDialog.cornerRadius.y / 2 : .0; + return AvesDialog( + title: title, + scrollableContent: [ + if (verticalPadding != 0) SizedBox(height: verticalPadding), + if (message != null) + Padding( + padding: const EdgeInsets.all(16), + child: Text(message), + ), + ...widget.options.entries.map((kv) { + final value = kv.key; + final title = kv.value; + final subtitle = widget.optionSubtitleBuilder?.call(value); + return SwitchListTile( + value: _selectedValues.contains(value), + onChanged: (v) { + if (v) { + _selectedValues.add(value); + } else { + _selectedValues.remove(value); + } + setState(() {}); + }, + title: Align( + alignment: Alignment.centerLeft, + child: Text(title), + ), + subtitle: subtitle != null + ? Text( + subtitle, + softWrap: false, + overflow: TextOverflow.fade, + ) + : null, + dense: widget.dense, + ); + }), + if (verticalPadding != 0) SizedBox(height: verticalPadding), + ], + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.maybeOf(context)?.pop(widget.options.keys.where(_selectedValues.contains).toList()), + child: Text(context.l10n.applyButtonLabel), + ), + ], + ); + } +} diff --git a/lib/widgets/dialogs/selection_dialogs/radio_list_tile.dart b/lib/widgets/dialogs/selection_dialogs/radio_list_tile.dart new file mode 100644 index 000000000..5ef9af327 --- /dev/null +++ b/lib/widgets/dialogs/selection_dialogs/radio_list_tile.dart @@ -0,0 +1,56 @@ +import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/common.dart'; +import 'package:flutter/material.dart'; + +class SelectionRadioListTile<T> extends StatelessWidget { + final T value; + final String title; + final TextBuilder<T>? optionSubtitleBuilder; + final bool needConfirmation; + final bool? dense; + final T Function() getGroupValue; + final void Function(T value) setGroupValue; + + const SelectionRadioListTile({ + super.key, + required this.value, + required this.title, + this.optionSubtitleBuilder, + required this.needConfirmation, + this.dense, + required this.getGroupValue, + required this.setGroupValue, + }); + + @override + Widget build(BuildContext context) { + final subtitle = optionSubtitleBuilder?.call(value); + return ReselectableRadioListTile<T>( + // key is expected by test driver + key: Key('$value'), + value: value, + groupValue: getGroupValue(), + onChanged: (v) { + if (needConfirmation) { + setGroupValue(v as T); + } else { + Navigator.maybeOf(context)?.pop(v); + } + }, + reselectable: true, + title: Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + subtitle: subtitle != null + ? Text( + subtitle, + softWrap: false, + overflow: TextOverflow.fade, + ) + : null, + dense: dense, + ); + } +} diff --git a/lib/widgets/dialogs/selection_dialogs/single_selection.dart b/lib/widgets/dialogs/selection_dialogs/single_selection.dart new file mode 100644 index 000000000..b9970cda4 --- /dev/null +++ b/lib/widgets/dialogs/selection_dialogs/single_selection.dart @@ -0,0 +1,80 @@ +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/common.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/radio_list_tile.dart'; +import 'package:flutter/material.dart'; + +class AvesSingleSelectionDialog<T> extends StatefulWidget { + static const routeName = '/dialog/selection'; + + final T initialValue; + final Map<T, String> options; + final TextBuilder<T>? optionSubtitleBuilder; + final String? title, message, confirmationButtonLabel; + final bool? dense; + + const AvesSingleSelectionDialog({ + super.key, + required this.initialValue, + required this.options, + this.optionSubtitleBuilder, + this.title, + this.message, + this.confirmationButtonLabel, + this.dense, + }); + + @override + State<AvesSingleSelectionDialog<T>> createState() => _AvesSingleSelectionDialogState<T>(); +} + +class _AvesSingleSelectionDialogState<T> extends State<AvesSingleSelectionDialog<T>> { + late T _selectedValue; + + @override + void initState() { + super.initState(); + _selectedValue = widget.initialValue; + } + + @override + Widget build(BuildContext context) { + final title = widget.title; + final message = widget.message; + final verticalPadding = (title == null && message == null) ? AvesDialog.cornerRadius.y / 2 : .0; + final confirmationButtonLabel = widget.confirmationButtonLabel; + final needConfirmation = confirmationButtonLabel != null; + return AvesDialog( + title: title, + scrollableContent: [ + if (verticalPadding != 0) SizedBox(height: verticalPadding), + if (message != null) + Padding( + padding: const EdgeInsets.all(16), + child: Text(message), + ), + ...widget.options.entries.map((kv) { + final radioValue = kv.key; + final radioTitle = kv.value; + return SelectionRadioListTile( + value: radioValue, + title: radioTitle, + optionSubtitleBuilder: widget.optionSubtitleBuilder, + needConfirmation: needConfirmation, + dense: widget.dense, + getGroupValue: () => _selectedValue, + setGroupValue: (v) => setState(() => _selectedValue = v), + ); + }), + if (verticalPadding != 0) SizedBox(height: verticalPadding), + ], + actions: [ + const CancelButton(), + if (needConfirmation) + TextButton( + onPressed: () => Navigator.maybeOf(context)?.pop(_selectedValue), + child: Text(confirmationButtonLabel), + ), + ], + ); + } +} diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 009499b59..b4aa1d56e 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -2,6 +2,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/transitions.dart'; @@ -168,18 +169,20 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with final label = ConstrainedBox( constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Row( - children: [ - Icon(icon), - const SizedBox(width: 16), - Expanded( - child: HighlightTitle( - title: title, - showHighlight: false, + child: FontSizeIconTheme( + child: Row( + children: [ + Icon(icon), + const SizedBox(width: 16), + Expanded( + child: HighlightTitle( + title: title, + showHighlight: false, + ), ), - ), - if (trailing != null) trailing, - ], + if (trailing != null) trailing, + ], + ), ), ); final selector = TextDropdownButton<T>( diff --git a/lib/widgets/dialogs/video_stream_selection_dialog.dart b/lib/widgets/dialogs/video_stream_selection_dialog.dart index fe14da528..5420ce3e3 100644 --- a/lib/widgets/dialogs/video_stream_selection_dialog.dart +++ b/lib/widgets/dialogs/video_stream_selection_dialog.dart @@ -1,6 +1,6 @@ -import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/ref/languages.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -117,7 +117,7 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog> final w = stream.width; final h = stream.height; if (w != null && h != null) { - return '$common • $w${ExtraAvesEntryProps.resolutionSeparator}$h'; + return '$common • $w${AText.resolutionSeparator}$h'; } } return common; diff --git a/lib/widgets/dialogs/wallpaper_settings_dialog.dart b/lib/widgets/dialogs/wallpaper_settings_dialog.dart index 0e6d46fad..9b3b759ff 100644 --- a/lib/widgets/dialogs/wallpaper_settings_dialog.dart +++ b/lib/widgets/dialogs/wallpaper_settings_dialog.dart @@ -1,12 +1,12 @@ import 'package:aves/model/device.dart'; -import 'package:aves/model/wallpaper_target.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/radio_list_tile.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -import 'aves_dialog.dart'; - class WallpaperSettingsDialog extends StatefulWidget { static const routeName = '/dialog/wallpaper_settings'; diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 5cd758ffb..259fa0728 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/apps.dart'; import 'package:aves/model/covers.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/filters/album.dart'; @@ -5,7 +6,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -13,6 +13,7 @@ import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -35,7 +36,7 @@ class AlbumListPage extends StatelessWidget { }, builder: (context, s, child) { return ValueListenableBuilder<bool>( - valueListenable: androidFileUtils.areAppNamesReadyNotifier, + valueListenable: appInventory.areAppNamesReadyNotifier, builder: (context, areAppNamesReady, child) { return StreamBuilder( stream: source.eventBus.on<AlbumsChangedEvent>(), 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 207802309..7c46687a4 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -1,19 +1,13 @@ import 'dart:io'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/chip_set_actions.dart'; -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/entry/entry.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/selection.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; -import 'package:aves/model/source/enums/view.dart'; import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/image_op_events.dart'; @@ -21,9 +15,8 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/widgets/aves_app.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; -import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; @@ -34,13 +27,14 @@ import 'package:aves/widgets/dialogs/filter_editors/rename_album_dialog.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with EntryStorageMixin, VaultAwareMixin { +class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with EntryStorageMixin { final Iterable<FilterGridItem<AlbumFilter>> _items; AlbumChipSetActionDelegate(Iterable<FilterGridItem<AlbumFilter>> items) : _items = items; @@ -128,7 +122,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with if (vaults.isVault(dirPath)) return true; // do not allow renaming volume root - final dir = VolumeRelativeDirectory.fromPath(dirPath); + final dir = androidFileUtils.relativeDirectoryFromPath(dirPath); return dir != null && dir.relativeDir.isNotEmpty; case ChipSetAction.hide: return hasSelection; @@ -163,7 +157,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with break; case ChipSetAction.lockVault: lockFilters(filters); - _browse(context); + browse(context); break; // single filter case ChipSetAction.rename: @@ -178,8 +172,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with super.onActionSelected(context, filters, action); } - void _browse(BuildContext context) => context.read<Selection<FilterGridItem<AlbumFilter>>>().browse(); - @override Future<void> configureView(BuildContext context) async { final initialValue = Tuple4( @@ -245,18 +237,21 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with source.createAlbum(directory); final filter = AlbumFilter(directory, source.getAlbumDisplayName(context, directory)); + // get navigator beforehand because + // local context may be deactivated when action is triggered after navigation + final navigator = Navigator.maybeOf(context); final showAction = SnackBarAction( label: l10n.showButtonLabel, onPressed: () async { // local context may be deactivated when action is triggered after navigation - final context = AvesApp.navigatorKey.currentContext; - if (context != null) { + if (navigator != null) { + final context = navigator.context; final highlightInfo = context.read<HighlightInfo>(); if (context.currentRouteName == AlbumListPage.routeName) { highlightInfo.trackItem(FilterGridItem(filter, null), highlightItem: filter); } else { highlightInfo.set(filter); - await Navigator.maybeOf(context)?.pushAndRemoveUntil( + await navigator.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: AlbumListPage.routeName), builder: (_) => const AlbumListPage(), @@ -282,7 +277,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with filters: kv.value.toSet(), enableBin: kv.key, )); - _browse(context); + browse(context); } Future<void> _doDelete({ @@ -306,7 +301,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with onSuccess: () { source.forgetNewAlbums(todoAlbums); source.cleanEmptyAlbums(emptyAlbums); - _browse(context); + browse(context); }, ); return; @@ -368,7 +363,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with final deletedOps = successOps.where((e) => !e.skipped).toSet(); final deletedUris = deletedOps.map((event) => event.uri).toSet(); await source.removeEntries(deletedUris, includeTrash: true); - _browse(context); + browse(context); source.resumeMonitoring(); final successCount = successOps.length; @@ -378,7 +373,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with } // cleanup - await storageService.deleteEmptyDirectories(filledAlbums); + await storageService.deleteEmptyRegularDirectories(filledAlbums); }, ); } @@ -388,7 +383,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with final album = filter.album; if (!vaults.isVault(album)) { - final dir = VolumeRelativeDirectory.fromPath(album); + final dir = androidFileUtils.relativeDirectoryFromPath(album); // do not allow renaming volume root if (dir == null || dir.relativeDir.isEmpty) return; @@ -447,7 +442,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with final successOps = processed.where((e) => e.success).toSet(); final movedOps = successOps.where((e) => !e.skipped).toSet(); await source.renameAlbum(album, destinationAlbum, todoEntries, movedOps); - _browse(context); + browse(context); source.resumeMonitoring(); final successCount = successOps.length; @@ -459,7 +454,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with } // cleanup - await storageService.deleteEmptyDirectories({album}); + await storageService.deleteEmptyRegularDirectories({album}); }, ); } @@ -493,7 +488,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with await _doRename(context, filter, newName); } else { await vaults.update(newDetails); - _browse(context); + browse(context); } } } diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart index 223d0aa38..b5fe5b64a 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; @@ -13,6 +12,7 @@ import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/places_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; 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 a9b9c20a2..111dd9960 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -1,5 +1,4 @@ import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/covers.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/filters/album.dart'; @@ -9,11 +8,10 @@ import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; -import 'package:aves/model/source/enums/view.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/view/view.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'; @@ -28,6 +26,7 @@ import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/stats/stats_page.dart'; import 'package:aves/widgets/viewer/slideshow_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -105,6 +104,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi return hasSelection && settings.pinnedFilters.containsAll(selectedFilters); case ChipSetAction.delete: case ChipSetAction.lockVault: + case ChipSetAction.showCountryStates: return false; // selecting (single filter) case ChipSetAction.setCover: @@ -148,6 +148,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi case ChipSetAction.pin: case ChipSetAction.unpin: case ChipSetAction.lockVault: + case ChipSetAction.showCountryStates: return hasSelection; // selecting (single filter) case ChipSetAction.rename: @@ -199,14 +200,15 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi break; case ChipSetAction.pin: settings.pinnedFilters = settings.pinnedFilters..addAll(filters); - _browse(context); + browse(context); break; case ChipSetAction.unpin: settings.pinnedFilters = settings.pinnedFilters..removeAll(filters); - _browse(context); + browse(context); break; case ChipSetAction.delete: case ChipSetAction.lockVault: + case ChipSetAction.showCountryStates: break; // selecting (single filter) case ChipSetAction.setCover: @@ -218,9 +220,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi } } - void _browse(BuildContext context) { - context.read<Selection<FilterGridItem<T>>?>()?.browse(); - } + void browse(BuildContext context) => context.read<Selection<FilterGridItem<T>>?>()?.browse(); Iterable<AvesEntry> _selectedEntries(BuildContext context, Set<dynamic> filters) { final source = context.read<CollectionSource>(); @@ -332,7 +332,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi settings.changeFilterVisibility(filters, false); - _browse(context); + browse(context); } void _setCover(BuildContext context, T filter) async { @@ -367,6 +367,6 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi color: selectedColor, ); - _browse(context); + browse(context); } } diff --git a/lib/widgets/filter_grids/common/action_delegates/country_set.dart b/lib/widgets/filter_grids/common/action_delegates/country_set.dart index 1e2ea6a31..0f3cc66dc 100644 --- a/lib/widgets/filter_grids/common/action_delegates/country_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/country_set.dart @@ -1,9 +1,15 @@ +import 'package:aves/app_mode.dart'; +import 'package:aves/geo/states.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; +import 'package:aves/widgets/filter_grids/states_page.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> { final Iterable<FilterGridItem<LocationFilter>> _items; @@ -30,4 +36,71 @@ class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> @override set tileLayout(TileLayout tileLayout) => settings.setTileLayout(CountryListPage.routeName, tileLayout); + + @override + bool isVisible( + ChipSetAction action, { + required AppMode appMode, + required bool isSelecting, + required int itemCount, + required Set<LocationFilter> selectedFilters, + }) { + switch (action) { + case ChipSetAction.showCountryStates: + return isSelecting; + default: + return super.isVisible( + action, + appMode: appMode, + isSelecting: isSelecting, + itemCount: itemCount, + selectedFilters: selectedFilters, + ); + } + } + + @override + bool canApply( + ChipSetAction action, { + required bool isSelecting, + required int itemCount, + required Set<LocationFilter> selectedFilters, + }) { + switch (action) { + case ChipSetAction.showCountryStates: + return selectedFilters.any((v) => GeoStates.stateCountryCodes.contains(v.code)); + default: + return super.canApply( + action, + isSelecting: isSelecting, + itemCount: itemCount, + selectedFilters: selectedFilters, + ); + } + } + + @override + void onActionSelected(BuildContext context, Set<LocationFilter> filters, ChipSetAction action) { + reportService.log('$action'); + switch (action) { + // single/multiple filters + case ChipSetAction.showCountryStates: + _showStates(context, filters); + browse(context); + break; + default: + break; + } + super.onActionSelected(context, filters, action); + } + + void _showStates(BuildContext context, Set<LocationFilter> filters) { + final countryCodes = filters.map((v) => v.code).where(GeoStates.stateCountryCodes.contains).whereNotNull().toSet(); + Navigator.maybeOf(context)?.push( + MaterialPageRoute( + settings: const RouteSettings(name: StateListPage.routeName), + builder: (_) => StateListPage(countryCodes: countryCodes), + ), + ); + } } diff --git a/lib/widgets/filter_grids/common/action_delegates/place_set.dart b/lib/widgets/filter_grids/common/action_delegates/place_set.dart index 35e5052bb..be9549e60 100644 --- a/lib/widgets/filter_grids/common/action_delegates/place_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/place_set.dart @@ -1,9 +1,9 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/places_page.dart'; +import 'package:aves_model/aves_model.dart'; class PlaceChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> { final Iterable<FilterGridItem<LocationFilter>> _items; diff --git a/lib/widgets/filter_grids/common/action_delegates/state_set.dart b/lib/widgets/filter_grids/common/action_delegates/state_set.dart new file mode 100644 index 000000000..8fb3d5cf9 --- /dev/null +++ b/lib/widgets/filter_grids/common/action_delegates/state_set.dart @@ -0,0 +1,33 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; +import 'package:aves/widgets/filter_grids/states_page.dart'; +import 'package:aves_model/aves_model.dart'; + +class StateChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> { + final Iterable<FilterGridItem<LocationFilter>> _items; + + StateChipSetActionDelegate(Iterable<FilterGridItem<LocationFilter>> items) : _items = items; + + @override + Iterable<FilterGridItem<LocationFilter>> get allItems => _items; + + @override + ChipSortFactor get sortFactor => settings.stateSortFactor; + + @override + set sortFactor(ChipSortFactor factor) => settings.stateSortFactor = factor; + + @override + bool get sortReverse => settings.stateSortReverse; + + @override + set sortReverse(bool value) => settings.stateSortReverse = value; + + @override + TileLayout get tileLayout => settings.getTileLayout(StateListPage.routeName); + + @override + set tileLayout(TileLayout tileLayout) => settings.setTileLayout(StateListPage.routeName, tileLayout); +} diff --git a/lib/widgets/filter_grids/common/action_delegates/tag_set.dart b/lib/widgets/filter_grids/common/action_delegates/tag_set.dart index e6403fd04..d669b6fab 100644 --- a/lib/widgets/filter_grids/common/action_delegates/tag_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/tag_set.dart @@ -1,9 +1,17 @@ +import 'package:aves/app_mode.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> { final Iterable<FilterGridItem<TagFilter>> _items; @@ -30,4 +38,68 @@ class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> { @override set tileLayout(TileLayout tileLayout) => settings.setTileLayout(TagListPage.routeName, tileLayout); + + @override + bool isVisible( + ChipSetAction action, { + required AppMode appMode, + required bool isSelecting, + required int itemCount, + required Set<TagFilter> selectedFilters, + }) { + final isMain = appMode == AppMode.main; + + switch (action) { + case ChipSetAction.delete: + return isMain && isSelecting && !settings.isReadOnly; + default: + return super.isVisible( + action, + appMode: appMode, + isSelecting: isSelecting, + itemCount: itemCount, + selectedFilters: selectedFilters, + ); + } + } + + @override + void onActionSelected(BuildContext context, Set<TagFilter> filters, ChipSetAction action) { + reportService.log('$action'); + switch (action) { + // single/multiple filters + case ChipSetAction.delete: + _delete(context, filters); + break; + default: + break; + } + super.onActionSelected(context, filters, action); + } + + Future<void> _delete(BuildContext context, Set<TagFilter> filters) async { + final source = context.read<CollectionSource>(); + final todoEntries = source.visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet(); + final todoTags = filters.map((v) => v.tag).toSet(); + + final confirmed = await showDialog<bool>( + context: context, + builder: (context) => AvesDialog( + content: Text(context.l10n.genericDangerWarningDialogMessage), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.maybeOf(context)?.pop(true), + child: Text(context.l10n.applyButtonLabel), + ), + ], + ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), + ); + if (confirmed == null || !confirmed) return; + + await EntrySetActionDelegate().removeTags(context, entries: todoEntries, tags: todoTags); + + browse(context); + } } diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index 1a1d7ffe6..7c752c228 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/query.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/togglers/title_search.dart'; import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; @@ -19,6 +19,7 @@ import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/query_bar.dart'; import 'package:aves/widgets/search/search_delegate.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -74,7 +75,7 @@ class FilterGridAppBar<T extends CollectionFilter, CSAD extends ChipSetActionDel } } -class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with SingleTickerProviderStateMixin { +class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with SingleTickerProviderStateMixin, WidgetsBindingObserver { final List<StreamSubscription> _subscriptions = []; late AnimationController _browseToSelectAnimation; final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false); @@ -104,6 +105,7 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct vsync: this, ); _isSelectingNotifier.addListener(_onActivityChanged); + WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) => _updateAppBarHeight()); } @@ -117,9 +119,16 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } + @override + void didChangeMetrics() { + // when text scale factor changes + _updateAppBarHeight(); + } + @override Widget build(BuildContext context) { final appMode = context.watch<ValueNotifier<AppMode>>().value; @@ -171,12 +180,13 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct } double get appBarContentHeight { - double height = kToolbarHeight; + final textScaleFactor = context.read<MediaQueryData>().textScaleFactor; + double height = kToolbarHeight * textScaleFactor; if (settings.useTvLayout) { height += CaptionedButton.getTelevisionButtonHeight(context); } if (context.read<Query>().enabled) { - height += FilterQueryBar.preferredHeight; + height += FilterQueryBar.getPreferredHeight(textScaleFactor); } return height; } @@ -226,7 +236,12 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct return InteractiveAppBarTitle( onTap: appMode.canNavigate ? _goToSearch : null, child: SourceStateAwareAppBarTitle( - title: Text(widget.title), + title: Text( + widget.title, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), source: source, ), ); @@ -318,46 +333,44 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct return [ ...quickActionButtons, - MenuIconTheme( - child: PopupMenuButton<ChipSetAction>( - itemBuilder: (context) { - final generalMenuItems = ChipSetActions.general.where(isVisible).map( - (action) => FilterGridAppBar.toMenuItem(context, action, enabled: canApply(action)), - ); + PopupMenuButton<ChipSetAction>( + itemBuilder: (context) { + final generalMenuItems = ChipSetActions.general.where(isVisible).map( + (action) => FilterGridAppBar.toMenuItem(context, action, enabled: canApply(action)), + ); - final browsingMenuActions = ChipSetActions.browsing.where((v) => !browsingQuickActions.contains(v)); - final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v)); - final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<ChipSetAction?>[], (prev, v) { - if (v == null && (prev.isEmpty || prev.last == null)) return prev; - return [...prev, v]; - }); - if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) { - contextualMenuActions.removeLast(); - } + final browsingMenuActions = ChipSetActions.browsing.where((v) => !browsingQuickActions.contains(v)); + final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v)); + final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<ChipSetAction?>[], (prev, v) { + if (v == null && (prev.isEmpty || prev.last == null)) return prev; + return [...prev, v]; + }); + if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) { + contextualMenuActions.removeLast(); + } - return [ - ...generalMenuItems, - if (contextualMenuActions.isNotEmpty) ...[ - const PopupMenuDivider(), - ...contextualMenuActions.map( - (action) { - if (action == null) return const PopupMenuDivider(); - return FilterGridAppBar.toMenuItem(context, action, enabled: canApply(action)); - }, - ), - ], - ]; - }, - onSelected: (action) async { - // remove focus, if any, to prevent the keyboard from showing up - // after the user is done with the popup menu - FocusManager.instance.primaryFocus?.unfocus(); + return [ + ...generalMenuItems, + if (contextualMenuActions.isNotEmpty) ...[ + const PopupMenuDivider(), + ...contextualMenuActions.map( + (action) { + if (action == null) return const PopupMenuDivider(); + return FilterGridAppBar.toMenuItem(context, action, enabled: canApply(action)); + }, + ), + ], + ]; + }, + onSelected: (action) async { + // remove focus, if any, to prevent the keyboard from showing up + // after the user is done with the popup menu + FocusManager.instance.primaryFocus?.unfocus(); - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - _onActionSelected(context, action, actionDelegate); - }, - ), + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + _onActionSelected(context, action, actionDelegate); + }, ), ]; } diff --git a/lib/widgets/filter_grids/common/covered_filter_chip.dart b/lib/widgets/filter_grids/common/covered_filter_chip.dart index 5187af03e..a2e066edf 100644 --- a/lib/widgets/filter_grids/common/covered_filter_chip.dart +++ b/lib/widgets/filter_grids/common/covered_filter_chip.dart @@ -12,8 +12,9 @@ import 'package:aves/model/source/tag.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/text.dart'; +import 'package:aves/model/apps.dart'; import 'package:aves/utils/android_file_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'; import 'package:aves/widgets/common/thumbnail/image.dart'; @@ -78,7 +79,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget { } case LocationFilter: { - final countryCode = (filter as LocationFilter).countryCode; + final countryCode = (filter as LocationFilter).code; return StreamBuilder<CountrySummaryInvalidatedEvent>( stream: source.eventBus.on<CountrySummaryInvalidatedEvent>().where((event) => event.countryCodes == null || event.countryCodes!.contains(countryCode)), builder: (context, snapshot) => _buildChip(context, source), @@ -108,7 +109,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget { if (_filter is AlbumFilter) { // when we asynchronously fetch installed app names, // album filters themselves do not change, but decoration derived from it does - chipKey = ValueKey(androidFileUtils.areAppNamesReadyNotifier.value); + chipKey = ValueKey(appInventory.areAppNamesReadyNotifier.value); } return AvesFilterChip( key: chipKey, @@ -214,7 +215,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget { ), ), Text( - locked ? Constants.overlayUnknown : numberFormat.format(source.count(filter)), + locked ? AText.valueNotAvailable : numberFormat.format(source.count(filter)), style: TextStyle( color: _detailColor(context), fontSize: fontSize, diff --git a/lib/widgets/filter_grids/common/draggable_thumb_label.dart b/lib/widgets/filter_grids/common/draggable_thumb_label.dart index 75ace8365..ab0f9941f 100644 --- a/lib/widgets/filter_grids/common/draggable_thumb_label.dart +++ b/lib/widgets/filter_grids/common/draggable_thumb_label.dart @@ -1,9 +1,9 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/draggable_thumb_label.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 26d2c0b65..168e616f8 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -8,7 +8,6 @@ import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; @@ -43,6 +42,7 @@ import 'package:aves/widgets/filter_grids/common/section_layout.dart'; import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:aves/widgets/navigation/tv_rail.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -485,9 +485,31 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi @override void initState() { super.initState(); + _registerWidget(widget); WidgetsBinding.instance.addPostFrameCallback((_) => _checkInitHighlight()); } + @override + void didUpdateWidget(covariant _FilterSectionedContent<T> oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(_FilterSectionedContent<T> widget) { + widget.appBarHeightNotifier.addListener(_onAppBarHeightChanged); + } + + void _unregisterWidget(_FilterSectionedContent<T> widget) { + widget.appBarHeightNotifier.removeListener(_onAppBarHeightChanged); + } + @override Widget build(BuildContext context) { final scrollView = AnimationLimiter( @@ -527,6 +549,8 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi ); } + void _onAppBarHeightChanged() => setState(() {}); + Future<void> _checkInitHighlight() async { final highlightInfo = context.read<HighlightInfo>(); final filter = highlightInfo.clear(); @@ -631,7 +655,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget { return settings.useTvLayout ? scrollView : _buildDraggableScrollView(scrollView); } - Widget _buildDraggableScrollView(ScrollView scrollView) { + Widget _buildDraggableScrollView(Widget scrollView) { return ValueListenableBuilder<double>( valueListenable: appBarHeightNotifier, builder: (context, appBarHeight, child) { @@ -672,7 +696,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget { ); } - ScrollView _buildScrollView(BuildContext context) { + Widget _buildScrollView(BuildContext context) { return CustomScrollView( key: scrollableKey, controller: scrollController, diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index c8fde49e0..7f235c29f 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -1,6 +1,5 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; @@ -8,6 +7,7 @@ import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart' import 'package:aves/widgets/filter_grids/common/app_bar.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class FilterNavigationPage<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends StatefulWidget { diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index 5322e6f53..4ac473b8c 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -4,7 +4,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; @@ -15,6 +14,7 @@ import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/filter_chip_grid_decorator.dart'; import 'package:aves/widgets/filter_grids/common/list_details.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/filter_grids/common/list_details.dart b/lib/widgets/filter_grids/common/list_details.dart index d7cacb7d7..37e6224c4 100644 --- a/lib/widgets/filter_grids/common/list_details.dart +++ b/lib/widgets/filter_grids/common/list_details.dart @@ -4,8 +4,9 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; @@ -87,7 +88,7 @@ class FilterListDetails<T extends CollectionFilter> extends StatelessWidget { final locale = context.l10n.localeName; final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat); final date = entry?.bestDate; - final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown; + final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; Widget leading = const Icon(AIcons.date); if (hasTitleLeading) { @@ -106,7 +107,7 @@ class FilterListDetails<T extends CollectionFilter> extends StatelessWidget { child: Text( dateText, style: detailsTheme.captionStyle, - strutStyle: Constants.overflowStrutStyle, + strutStyle: AStyles.overflowStrut, softWrap: false, overflow: TextOverflow.fade, ), @@ -157,7 +158,7 @@ class FilterListDetails<T extends CollectionFilter> extends StatelessWidget { Text( '${l10n.itemCount(source.count(filter))} • ${formatFileSize(locale, source.size(filter))}', style: detailsTheme.captionStyle, - strutStyle: Constants.overflowStrutStyle, + strutStyle: AStyles.overflowStrut, softWrap: false, overflow: TextOverflow.fade, ), diff --git a/lib/widgets/filter_grids/common/list_details_theme.dart b/lib/widgets/filter_grids/common/list_details_theme.dart index 774faff0f..4176ff615 100644 --- a/lib/widgets/filter_grids/common/list_details_theme.dart +++ b/lib/widgets/filter_grids/common/list_details_theme.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:aves/theme/format.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; @@ -49,7 +49,7 @@ class FilterListDetailsTheme extends StatelessWidget { TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle), textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, - strutStyle: Constants.overflowStrutStyle, + strutStyle: AStyles.overflowStrut, )..layout(const BoxConstraints(), parentUsesSize: true)) .getMaxIntrinsicHeight(double.infinity); diff --git a/lib/widgets/filter_grids/common/query_bar.dart b/lib/widgets/filter_grids/common/query_bar.dart index 6ecffa88c..4f861b861 100644 --- a/lib/widgets/filter_grids/common/query_bar.dart +++ b/lib/widgets/filter_grids/common/query_bar.dart @@ -9,8 +9,6 @@ class FilterQueryBar<T extends CollectionFilter> extends StatelessWidget { final ValueNotifier<String> queryNotifier; final FocusNode focusNode; - static const preferredHeight = kToolbarHeight; - const FilterQueryBar({ super.key, required this.queryNotifier, @@ -19,8 +17,9 @@ class FilterQueryBar<T extends CollectionFilter> extends StatelessWidget { @override Widget build(BuildContext context) { + final textScaleFactor = context.select<MediaQueryData, double>((mq) => mq.textScaleFactor); return Container( - height: FilterQueryBar.preferredHeight, + height: getPreferredHeight(textScaleFactor), alignment: Alignment.topCenter, child: Selector<Selection<FilterGridItem<T>>, bool>( selector: (context, selection) => !selection.isSelecting, @@ -33,4 +32,6 @@ class FilterQueryBar<T extends CollectionFilter> extends StatelessWidget { ), ); } + + static double getPreferredHeight(double textScaleFactor) => QueryBar.getPreferredHeight(textScaleFactor); } diff --git a/lib/widgets/filter_grids/common/section_keys.dart b/lib/widgets/filter_grids/common/section_keys.dart index a93185c39..abca16740 100644 --- a/lib/widgets/filter_grids/common/section_keys.dart +++ b/lib/widgets/filter_grids/common/section_keys.dart @@ -1,8 +1,9 @@ import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/filter_grids/common/enums.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index c44d4b4f6..dcaf5e879 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -2,7 +2,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/location/country.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -10,6 +9,7 @@ import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/country_set.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/filter_grids/places_page.dart b/lib/widgets/filter_grids/places_page.dart index 5c75843cb..e29dc1c97 100644 --- a/lib/widgets/filter_grids/places_page.dart +++ b/lib/widgets/filter_grids/places_page.dart @@ -2,7 +2,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/location/place.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -10,6 +9,7 @@ import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/place_set.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/filter_grids/states_page.dart b/lib/widgets/filter_grids/states_page.dart new file mode 100644 index 000000000..03c4a7dd6 --- /dev/null +++ b/lib/widgets/filter_grids/states_page.dart @@ -0,0 +1,87 @@ +import 'package:aves/geo/states.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/location/place.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/state_set.dart'; +import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; +import 'package:aves/widgets/filter_grids/common/section_keys.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; + +class StateListPage extends StatelessWidget { + static const routeName = '/states'; + + final Set<String> countryCodes; + + const StateListPage({ + super.key, + required this.countryCodes, + }); + + @override + Widget build(BuildContext context) { + final source = context.read<CollectionSource>(); + return Selector<Settings, Tuple3<ChipSortFactor, bool, Set<CollectionFilter>>>( + selector: (context, s) => Tuple3(s.stateSortFactor, s.stateSortReverse, s.pinnedFilters), + shouldRebuild: (t1, t2) { + // `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN` + const eq = DeepCollectionEquality(); + return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2) && eq.equals(t1.item3, t2.item3)); + }, + builder: (context, s, child) { + return StreamBuilder( + stream: source.eventBus.on<PlacesChangedEvent>(), + builder: (context, snapshot) { + final gridItems = _getGridItems(source); + return FilterNavigationPage<LocationFilter, StateChipSetActionDelegate>( + source: source, + title: context.l10n.statePageTitle, + sortFactor: settings.stateSortFactor, + actionDelegate: StateChipSetActionDelegate(gridItems), + filterSections: _groupToSections(gridItems), + applyQuery: applyQuery, + emptyBuilder: () => EmptyContent( + icon: AIcons.state, + text: context.l10n.stateEmpty, + ), + ); + }, + ); + }, + ); + } + + List<FilterGridItem<LocationFilter>> applyQuery(BuildContext context, List<FilterGridItem<LocationFilter>> filters, String query) { + return filters.where((item) => item.filter.getLabel(context).toUpperCase().contains(query)).toList(); + } + + List<FilterGridItem<LocationFilter>> _getGridItems(CollectionSource source) { + final selectedStateCodes = countryCodes.expand((v) => GeoStates.stateCodesByCountryCode[v] ?? <String>{}).toSet(); + final filters = source.sortedStates.where((v) => selectedStateCodes.any(v.endsWith)).map((location) => LocationFilter(LocationLevel.state, location)).toSet(); + + return FilterNavigationPage.sort(settings.stateSortFactor, settings.stateSortReverse, source, filters); + } + + static Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _groupToSections(Iterable<FilterGridItem<LocationFilter>> sortedMapEntries) { + final pinned = settings.pinnedFilters.whereType<LocationFilter>(); + final byPin = groupBy<FilterGridItem<LocationFilter>, bool>(sortedMapEntries, (e) => pinned.contains(e.filter)); + final pinnedMapEntries = (byPin[true] ?? []); + final unpinnedMapEntries = (byPin[false] ?? []); + + return { + if (pinnedMapEntries.isNotEmpty || unpinnedMapEntries.isNotEmpty) + const ChipSectionKey(): [ + ...pinnedMapEntries, + ...unpinnedMapEntries, + ], + }; + } +} diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 23520b47f..f34d44ed1 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -2,7 +2,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -10,6 +9,7 @@ import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/tag_set.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 52ba45a4e..ff4defde4 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -1,23 +1,22 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; +import 'package:aves/model/app/permissions.dart'; +import 'package:aves/model/apps.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/catalog.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.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'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/services/analysis_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/global_search.dart'; import 'package:aves/services/intent_service.dart'; import 'package:aves/services/widget_service.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; @@ -31,6 +30,7 @@ import 'package:aves/widgets/settings/screen_saver_settings_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/screen_saver_page.dart'; import 'package:aves/widgets/wallpaper_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -73,6 +73,7 @@ class _HomePageState extends State<HomePage> { static const intentDataKeyMimeType = 'mimeType'; static const intentDataKeyPage = 'page'; static const intentDataKeyQuery = 'query'; + static const intentDataKeySafeMode = 'safeMode'; static const intentDataKeyUri = 'uri'; static const intentDataKeyWidgetId = 'widgetId'; @@ -97,21 +98,18 @@ class _HomePageState extends State<HomePage> { if (await windowService.isActivity()) { // do not check whether permission was granted, because some app stores // hide in some countries apps that force quit on permission denial - await [ - ...Constants.storagePermissions, - // to access media with unredacted metadata with scoped storage (Android >=10) - Permission.accessMediaLocation, - ].request(); + await Permissions.mediaAccess.request(); } var appMode = AppMode.main; final intentData = widget.intentData ?? await IntentService.getIntentData(); + final safeMode = intentData[intentDataKeySafeMode] ?? false; final intentAction = intentData[intentDataKeyAction]; _initialFilters = null; await androidFileUtils.init(); if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction) && settings.isInstalledAppAccessAllowed) { - unawaited(androidFileUtils.initAppNames()); + unawaited(appInventory.initAppNames()); } if (intentData.isNotEmpty) { @@ -209,6 +207,7 @@ class _HomePageState extends State<HomePage> { final source = context.read<CollectionSource>(); await source.init( loadTopEntriesFirst: settings.homePage == HomePageSetting.collection, + canAnalyze: !safeMode, ); break; case AppMode.screenSaver: diff --git a/lib/widgets/home_widget.dart b/lib/widgets/home_widget.dart index 69f8c61b1..1e81d6e77 100644 --- a/lib/widgets/home_widget.dart +++ b/lib/widgets/home_widget.dart @@ -4,10 +4,10 @@ import 'dart:ui' as ui; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/images.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/widget_shape.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class HomeWidgetPainter { @@ -17,7 +17,7 @@ class HomeWidgetPainter { static const backgroundGradient = LinearGradient( begin: Alignment.bottomLeft, end: Alignment.topRight, - colors: Constants.boraBoraGradientColors, + colors: AColors.boraBoraGradient, ); HomeWidgetPainter({ diff --git a/lib/widgets/map/address_row.dart b/lib/widgets/map/address_row.dart new file mode 100644 index 000000000..407e24aab --- /dev/null +++ b/lib/widgets/map/address_row.dart @@ -0,0 +1,104 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/location.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'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'info_row.dart'; + +class MapAddressRow extends StatefulWidget { + final AvesEntry? entry; + + const MapAddressRow({ + super.key, + required this.entry, + }); + + @override + State<MapAddressRow> createState() => _MapAddressRowState(); +} + +class _MapAddressRowState extends State<MapAddressRow> { + final ValueNotifier<String?> _addressLineNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _updateAddress(); + } + + @override + void didUpdateWidget(covariant MapAddressRow oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.entry != widget.entry) { + _updateAddress(); + } + } + + @override + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.centerStart, + // addresses can include non-latin scripts with inconsistent line height, + // which is especially an issue for relayout/painting of heavy Google map, + // so we give extra height to give breathing room to the text and stabilize layout + height: Theme.of(context).textTheme.bodyMedium!.fontSize! * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor) * 2, + child: ValueListenableBuilder<String?>( + valueListenable: _addressLineNotifier, + builder: (context, addressLine, child) { + final entry = widget.entry; + final location = addressLine ?? + (entry == null + ? AText.valueNotAvailable + : entry.hasAddress + ? entry.shortAddress + : settings.coordinateFormat.format(context.l10n, entry.latLng!)); + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: MapInfoRow.iconPadding), + child: Icon(AIcons.location, size: MapInfoRow.getIconSize(context)), + ), + alignment: PlaceholderAlignment.middle, + ), + TextSpan(text: location), + ], + ), + strutStyle: AStyles.overflowStrut, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ); + }, + ), + ); + } + + Future<void> _updateAddress() async { + final entry = widget.entry; + final addressLine = await _getAddressLine(entry); + if (mounted && entry == widget.entry) { + _addressLineNotifier.value = addressLine; + } + } + + Future<String?> _getAddressLine(AvesEntry? entry) async { + if (entry != null && await availability.canLocatePlaces) { + final addresses = await GeocodingService.getAddress(entry.latLng!, settings.appliedLocale); + if (addresses.isNotEmpty) { + final address = addresses.first; + return address.addressLine; + } + } + return null; + } +} diff --git a/lib/widgets/map/date_row.dart b/lib/widgets/map/date_row.dart new file mode 100644 index 000000000..430a731fc --- /dev/null +++ b/lib/widgets/map/date_row.dart @@ -0,0 +1,45 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/theme/format.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/map/info_row.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class MapDateRow extends StatelessWidget { + final AvesEntry? entry; + + const MapDateRow({ + super.key, + required this.entry, + }); + + @override + Widget build(BuildContext context) { + final locale = context.l10n.localeName; + final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat); + + final date = entry?.bestDate; + final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: MapInfoRow.iconPadding), + child: Icon(AIcons.date, size: MapInfoRow.getIconSize(context)), + ), + alignment: PlaceholderAlignment.middle, + ), + TextSpan(text: dateText), + ], + ), + strutStyle: AStyles.overflowStrut, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ); + } +} diff --git a/lib/widgets/map/info_row.dart b/lib/widgets/map/info_row.dart new file mode 100644 index 000000000..fc58e1387 --- /dev/null +++ b/lib/widgets/map/info_row.dart @@ -0,0 +1,63 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/widgets/map/address_row.dart'; +import 'package:aves/widgets/map/date_row.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class MapInfoRow extends StatelessWidget { + final ValueNotifier<AvesEntry?> entryNotifier; + + static const double iconPadding = 8.0; + static const double _interRowPadding = 2.0; + + const MapInfoRow({ + super.key, + required this.entryNotifier, + }); + + @override + Widget build(BuildContext context) { + final orientation = context.select<MediaQueryData, Orientation>((v) => v.orientation); + + return ValueListenableBuilder<AvesEntry?>( + valueListenable: entryNotifier, + builder: (context, entry, child) { + final content = orientation == Orientation.portrait + ? [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MapAddressRow(entry: entry), + const SizedBox(height: _interRowPadding), + MapDateRow(entry: entry), + ], + ), + ), + ] + : [ + MapDateRow(entry: entry), + Expanded( + child: MapAddressRow(entry: entry), + ), + ]; + + return Opacity( + opacity: entry != null ? 1 : 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: iconPadding), + const DotMarker(), + ...content, + ], + ), + ); + }, + ); + } + + static double getIconSize(BuildContext context) => 16.0 * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor); +} diff --git a/lib/widgets/map/map_info_row.dart b/lib/widgets/map/map_info_row.dart deleted file mode 100644 index 67a600cbf..000000000 --- a/lib/widgets/map/map_info_row.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:aves/model/entry/entry.dart'; -import 'package:aves/model/entry/extensions/location.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'; -import 'package:aves/theme/format.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves_map/aves_map.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class MapInfoRow extends StatelessWidget { - final ValueNotifier<AvesEntry?> entryNotifier; - - static const double iconPadding = 8.0; - static const double iconSize = 16.0; - static const double _interRowPadding = 2.0; - - const MapInfoRow({ - super.key, - required this.entryNotifier, - }); - - @override - Widget build(BuildContext context) { - final orientation = context.select<MediaQueryData, Orientation>((v) => v.orientation); - - return ValueListenableBuilder<AvesEntry?>( - valueListenable: entryNotifier, - builder: (context, entry, child) { - final content = orientation == Orientation.portrait - ? [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _AddressRow(entry: entry), - const SizedBox(height: _interRowPadding), - _DateRow(entry: entry), - ], - ), - ), - ] - : [ - _DateRow(entry: entry), - Expanded( - child: _AddressRow(entry: entry), - ), - ]; - - return Opacity( - opacity: entry != null ? 1 : 0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: iconPadding), - const DotMarker(), - ...content, - ], - ), - ); - }, - ); - } -} - -class _AddressRow extends StatefulWidget { - final AvesEntry? entry; - - const _AddressRow({ - required this.entry, - }); - - @override - State<_AddressRow> createState() => _AddressRowState(); -} - -class _AddressRowState extends State<_AddressRow> { - final ValueNotifier<String?> _addressLineNotifier = ValueNotifier(null); - - @override - void initState() { - super.initState(); - _updateAddress(); - } - - @override - void didUpdateWidget(covariant _AddressRow oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.entry != widget.entry) { - _updateAddress(); - } - } - - @override - Widget build(BuildContext context) { - final entry = widget.entry; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: MapInfoRow.iconPadding), - const Icon(AIcons.location, size: MapInfoRow.iconSize), - const SizedBox(width: MapInfoRow.iconPadding), - Expanded( - child: Container( - alignment: AlignmentDirectional.centerStart, - // addresses can include non-latin scripts with inconsistent line height, - // which is especially an issue for relayout/painting of heavy Google map, - // so we give extra height to give breathing room to the text and stabilize layout - height: Theme.of(context).textTheme.bodyMedium!.fontSize! * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor) * 2, - child: ValueListenableBuilder<String?>( - valueListenable: _addressLineNotifier, - builder: (context, addressLine, child) { - final location = addressLine ?? - (entry == null - ? Constants.overlayUnknown - : entry.hasAddress - ? entry.shortAddress - : settings.coordinateFormat.format(context.l10n, entry.latLng!)); - return Text( - location, - strutStyle: Constants.overflowStrutStyle, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ); - }, - ), - ), - ), - ], - ); - } - - Future<void> _updateAddress() async { - final entry = widget.entry; - final addressLine = await _getAddressLine(entry); - if (mounted && entry == widget.entry) { - _addressLineNotifier.value = addressLine; - } - } - - Future<String?> _getAddressLine(AvesEntry? entry) async { - if (entry != null && await availability.canLocatePlaces) { - final addresses = await GeocodingService.getAddress(entry.latLng!, settings.appliedLocale); - if (addresses.isNotEmpty) { - final address = addresses.first; - return address.addressLine; - } - } - return null; - } -} - -class _DateRow extends StatelessWidget { - final AvesEntry? entry; - - const _DateRow({ - required this.entry, - }); - - @override - Widget build(BuildContext context) { - final locale = context.l10n.localeName; - final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat); - - final date = entry?.bestDate; - final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown; - return Row( - children: [ - const SizedBox(width: MapInfoRow.iconPadding), - const Icon(AIcons.date, size: MapInfoRow.iconSize), - const SizedBox(width: MapInfoRow.iconPadding), - Text( - dateText, - strutStyle: Constants.overflowStrutStyle, - ), - ], - ); - } -} diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 5cf00c41b..63a463d09 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/map_actions.dart'; -import 'package:aves/model/actions/map_cluster_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/filters/coordinate.dart'; @@ -15,26 +13,26 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/debouncer.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; -import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; import 'package:aves/widgets/common/map/map_action_delegate.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart'; -import 'package:aves/widgets/common/thumbnail/scroller.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; -import 'package:aves/widgets/map/map_info_row.dart'; +import 'package:aves/widgets/map/scroller.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -100,9 +98,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin final ValueNotifier<int?> _selectedIndexNotifier = ValueNotifier(0); final ValueNotifier<CollectionLens?> _regionCollectionNotifier = ValueNotifier(null); final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null); - final ValueNotifier<AvesEntry?> _dotEntryNotifier = ValueNotifier(null), _infoEntryNotifier = ValueNotifier(null); + final ValueNotifier<AvesEntry?> _dotEntryNotifier = ValueNotifier(null); final ValueNotifier<double> _overlayOpacityNotifier = ValueNotifier(1); - final Debouncer _infoDebouncer = Debouncer(delay: Durations.mapInfoDebounceDelay); final ValueNotifier<bool> _overlayVisible = ValueNotifier(true); late AnimationController _overlayAnimationController; late Animation<double> _overlayScale, _scrollerSize; @@ -124,8 +121,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin }); } - _dotEntryNotifier.addListener(_onSelectedEntryChanged); - _overlayAnimationController = AnimationController( duration: context.read<DurationsData>().viewerOverlayAnimation, vsync: this, @@ -165,7 +160,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin ..forEach((sub) => sub.cancel()) ..clear(); _dotEntryNotifier.value?.metadataChangeNotifier.removeListener(_onMarkerEntryMetadataChanged); - _dotEntryNotifier.removeListener(_onSelectedEntryChanged); _overlayAnimationController.dispose(); _overlayVisible.removeListener(_onOverlayVisibleChanged); _mapController.dispose(); @@ -228,8 +222,13 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin children: [ const SizedBox(height: 8), const Divider(height: 0), - _buildOverlayController(), - _buildScroller(), + _buildOverlayControls(), + MapEntryScroller( + regionCollectionNotifier: _regionCollectionNotifier, + dotEntryNotifier: _dotEntryNotifier, + selectedIndexNotifier: _selectedIndexNotifier, + onTap: (index) => _goToViewer(_getRegionEntry(index)), + ), ], ), ), @@ -299,7 +298,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin return child; } - Widget _buildOverlayController() { + Widget _buildOverlayControls() { if (widget.overlayEntry == null) return const SizedBox(); return Padding( @@ -325,63 +324,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin ); } - Widget _buildScroller() { - return Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SafeArea( - top: false, - bottom: false, - child: MapInfoRow(entryNotifier: _infoEntryNotifier), - ), - const SizedBox(height: 8), - Selector<MediaQueryData, double>( - selector: (context, mq) => mq.size.width, - builder: (context, mqWidth, child) => ValueListenableBuilder<CollectionLens?>( - valueListenable: _regionCollectionNotifier, - builder: (context, regionCollection, child) { - return AnimatedBuilder( - // update when entries are added/removed - animation: regionCollection ?? ChangeNotifier(), - builder: (context, child) { - final regionEntries = regionCollection?.sortedEntries ?? []; - return ThumbnailScroller( - availableWidth: mqWidth, - entryCount: regionEntries.length, - entryBuilder: (index) => index < regionEntries.length ? regionEntries[index] : null, - indexNotifier: _selectedIndexNotifier, - onTap: _onThumbnailTap, - heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.id]), - highlightable: true, - showLocation: false, - ); - }, - ); - }, - ), - ), - ], - ), - Positioned.fill( - child: ValueListenableBuilder<CollectionLens?>( - valueListenable: _regionCollectionNotifier, - builder: (context, regionCollection, child) { - return regionCollection != null && regionCollection.isEmpty - ? EmptyContent( - text: context.l10n.mapEmptyRegion, - alignment: Alignment.center, - fontSize: 18, - ) - : const SizedBox(); - }, - ), - ), - ], - ); - } - void _onIdle(ZoomedBounds bounds) { _regionFilter = CoordinateFilter(bounds.sw, bounds.ne); _updateRegionCollection(); @@ -431,8 +373,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin return null; } - void _onThumbnailTap(int index) => _goToViewer(_getRegionEntry(index)); - void _onThumbnailIndexChanged() => _onEntrySelected(_getRegionEntry(_selectedIndexNotifier.value)); void _onEntrySelected(AvesEntry? selectedEntry) { @@ -446,15 +386,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin _dotLocationNotifier.value = _dotEntryNotifier.value?.latLng; } - void _onSelectedEntryChanged() { - final selectedEntry = _dotEntryNotifier.value; - if (_infoEntryNotifier.value == null || selectedEntry == null) { - _infoEntryNotifier.value = selectedEntry; - } else { - _infoDebouncer(() => _infoEntryNotifier.value = selectedEntry); - } - } - void _goToViewer(AvesEntry? initialEntry) { if (initialEntry == null) return; @@ -567,7 +498,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin PopupMenuItem<MapClusterAction> _buildMenuItem(MapClusterAction action) { return PopupMenuItem( value: action, - child: MenuIconTheme( + child: FontSizeIconTheme( child: MenuRow( text: action.getText(context), icon: action.getIcon(), diff --git a/lib/widgets/map/scroller.dart b/lib/widgets/map/scroller.dart new file mode 100644 index 000000000..54c9a1c6b --- /dev/null +++ b/lib/widgets/map/scroller.dart @@ -0,0 +1,127 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/common/thumbnail/scroller.dart'; +import 'package:aves/widgets/map/info_row.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class MapEntryScroller extends StatefulWidget { + final ValueNotifier<CollectionLens?> regionCollectionNotifier; + final ValueNotifier<AvesEntry?> dotEntryNotifier; + final ValueNotifier<int?> selectedIndexNotifier; + final void Function(int index) onTap; + + const MapEntryScroller({ + super.key, + required this.regionCollectionNotifier, + required this.dotEntryNotifier, + required this.selectedIndexNotifier, + required this.onTap, + }); + + @override + State<MapEntryScroller> createState() => _MapEntryScrollerState(); +} + +class _MapEntryScrollerState extends State<MapEntryScroller> { + final ValueNotifier<AvesEntry?> _infoEntryNotifier = ValueNotifier(null); + final Debouncer _infoDebouncer = Debouncer(delay: Durations.mapInfoDebounceDelay); + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant MapEntryScroller oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(MapEntryScroller widget) { + widget.dotEntryNotifier.addListener(_onSelectedEntryChanged); + } + + void _unregisterWidget(MapEntryScroller widget) { + widget.dotEntryNotifier.removeListener(_onSelectedEntryChanged); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SafeArea( + top: false, + bottom: false, + child: MapInfoRow(entryNotifier: _infoEntryNotifier), + ), + const SizedBox(height: 8), + Selector<MediaQueryData, double>( + selector: (context, mq) => mq.size.width, + builder: (context, mqWidth, child) => ValueListenableBuilder<CollectionLens?>( + valueListenable: widget.regionCollectionNotifier, + builder: (context, regionCollection, child) { + return AnimatedBuilder( + // update when entries are added/removed + animation: regionCollection ?? ChangeNotifier(), + builder: (context, child) { + final regionEntries = regionCollection?.sortedEntries ?? []; + return ThumbnailScroller( + availableWidth: mqWidth, + entryCount: regionEntries.length, + entryBuilder: (index) => index < regionEntries.length ? regionEntries[index] : null, + indexNotifier: widget.selectedIndexNotifier, + onTap: widget.onTap, + heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.id]), + highlightable: true, + showLocation: false, + ); + }, + ); + }, + ), + ), + ], + ), + Positioned.fill( + child: ValueListenableBuilder<CollectionLens?>( + valueListenable: widget.regionCollectionNotifier, + builder: (context, regionCollection, child) { + return regionCollection != null && regionCollection.isEmpty + ? EmptyContent( + text: context.l10n.mapEmptyRegion, + alignment: Alignment.center, + fontSize: 18, + ) + : const SizedBox(); + }, + ), + ), + ], + ); + } + + void _onSelectedEntryChanged() { + final selectedEntry = widget.dotEntryNotifier.value; + if (_infoEntryNotifier.value == null || selectedEntry == null) { + _infoEntryNotifier.value = selectedEntry; + } else { + _infoDebouncer(() => _infoEntryNotifier.value = selectedEntry); + } + } +} diff --git a/lib/widgets/navigation/drawer/app_drawer.dart b/lib/widgets/navigation/drawer/app_drawer.dart index 3e22c1b43..8bb449524 100644 --- a/lib/widgets/navigation/drawer/app_drawer.dart +++ b/lib/widgets/navigation/drawer/app_drawer.dart @@ -26,6 +26,7 @@ import 'package:aves/widgets/navigation/drawer/collection_nav_tile.dart'; import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart'; import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/settings/settings_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/navigation/tv_rail.dart b/lib/widgets/navigation/tv_rail.dart index 7f92fe603..a408e2a50 100644 --- a/lib/widgets/navigation/tv_rail.dart +++ b/lib/widgets/navigation/tv_rail.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.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'; @@ -18,6 +17,7 @@ import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart'; import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/settings/settings_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index d982aeeaf..fb1fd59d4 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -136,6 +136,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va _buildDateFilters(context, containQuery), _buildAlbumFilters(containQuery), _buildCountryFilters(containQuery), + _buildStateFilters(containQuery), _buildPlaceFilters(containQuery), _buildTagFilters(containQuery), _buildRatingFilters(context, containQuery), @@ -223,6 +224,19 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va ); } + Widget _buildStateFilters(_ContainQuery containQuery) { + return StreamBuilder( + stream: source.eventBus.on<PlacesChangedEvent>(), + builder: (context, snapshot) { + return _buildFilterRow( + context: context, + title: context.l10n.searchStatesSectionTitle, + filters: source.sortedStates.where(containQuery).map((s) => LocationFilter(LocationLevel.state, s)).toList(), + ); + }, + ); + } + Widget _buildPlaceFilters(_ContainQuery containQuery) { return StreamBuilder( stream: source.eventBus.on<PlacesChangedEvent>(), diff --git a/lib/widgets/settings/accessibility/accessibility.dart b/lib/widgets/settings/accessibility/accessibility.dart index 17d963d7f..f7f558f88 100644 --- a/lib/widgets/settings/accessibility/accessibility.dart +++ b/lib/widgets/settings/accessibility/accessibility.dart @@ -1,15 +1,15 @@ import 'dart:async'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/accessibility/time_to_take_action.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/accessibility/time_to_take_action.dart b/lib/widgets/settings/accessibility/time_to_take_action.dart index 137867c57..34455b658 100644 --- a/lib/widgets/settings/accessibility/time_to_take_action.dart +++ b/lib/widgets/settings/accessibility/time_to_take_action.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/accessibility_service.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class TimeToTakeActionTile extends StatefulWidget { diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index 120e2e55d..0bec2c037 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves_utils/aves_utils.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; @@ -12,6 +12,7 @@ import 'package:aves/widgets/settings/common/quick_actions/action_panel.dart'; import 'package:aves/widgets/settings/common/quick_actions/available_actions.dart'; import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart'; import 'package:aves/widgets/settings/common/quick_actions/quick_actions.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -151,7 +152,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi padding: const EdgeInsets.all(16), child: Row( children: [ - const Icon(AIcons.info), + const FontSizeIconTheme(child: Icon(AIcons.info)), const SizedBox(width: 16), Expanded(child: Text(widget.bannerText)), ], @@ -162,7 +163,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi padding: const EdgeInsets.only(left: 16, top: 16, right: 16), child: Text( context.l10n.settingsViewerQuickActionEditorDisplayedButtonsSectionTitle, - style: Constants.knownTitleTextStyle, + style: AStyles.knownTitleText, ), ), ValueListenableBuilder<bool>( @@ -239,7 +240,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( context.l10n.settingsViewerQuickActionEditorAvailableButtonsSectionTitle, - style: Constants.knownTitleTextStyle, + style: AStyles.knownTitleText, ), ), ValueListenableBuilder<bool>( diff --git a/lib/widgets/settings/common/tile_leading.dart b/lib/widgets/settings/common/tile_leading.dart index 55152617a..1fe84bac9 100644 --- a/lib/widgets/settings/common/tile_leading.dart +++ b/lib/widgets/settings/common/tile_leading.dart @@ -1,5 +1,5 @@ import 'package:aves/theme/durations.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; @@ -30,7 +30,7 @@ class SettingsTileLeading extends StatelessWidget { icon, size: 18, color: DefaultTextStyle.of(context).style.color, - shadows: Theme.of(context).brightness == Brightness.dark ? Constants.embossShadows : null, + shadows: Theme.of(context).brightness == Brightness.dark ? AStyles.embossShadows : null, ), ); } diff --git a/lib/widgets/settings/common/tiles.dart b/lib/widgets/settings/common/tiles.dart index a1892e6fc..c9fabc4c7 100644 --- a/lib/widgets/settings/common/tiles.dart +++ b/lib/widgets/settings/common/tiles.dart @@ -1,10 +1,13 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_caption.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/dialogs/duration_dialog.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/common.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/multi_selection.dart'; +import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -82,7 +85,7 @@ class SettingsSwitchListTile extends StatelessWidget { } } -class SettingsSelectionListTile<T extends Enum> extends StatelessWidget { +class SettingsSelectionListTile<T> extends StatelessWidget { final List<T> values; final String Function(BuildContext, T) getName; final T Function(BuildContext, Settings) selector; @@ -123,7 +126,7 @@ class SettingsSelectionListTile<T extends Enum> extends StatelessWidget { subtitle: AvesCaption(getName(context, current)), onTap: () => showSelectionDialog<T>( context: context, - builder: (context) => AvesSelectionDialog<T>( + builder: (context) => AvesSingleSelectionDialog<T>( initialValue: current, options: Map.fromEntries(values.map((v) => MapEntry(v, getName(context, v)))), optionSubtitleBuilder: optionSubtitleBuilder, @@ -137,6 +140,62 @@ class SettingsSelectionListTile<T extends Enum> extends StatelessWidget { } } +class SettingsMultiSelectionListTile<T> extends StatelessWidget { + final List<T> values; + final String Function(BuildContext, T) getName; + final List<T> Function(BuildContext, Settings) selector; + final ValueChanged<List<T>> onSelection; + final String tileTitle, noneSubtitle; + final WidgetBuilder? trailingBuilder; + final String? dialogTitle; + final TextBuilder<T>? optionSubtitleBuilder; + + const SettingsMultiSelectionListTile({ + super.key, + required this.values, + required this.getName, + required this.selector, + required this.onSelection, + required this.tileTitle, + required this.noneSubtitle, + this.trailingBuilder, + this.dialogTitle, + this.optionSubtitleBuilder, + }); + + @override + Widget build(BuildContext context) { + return Selector<Settings, List<T>>( + selector: selector, + builder: (context, current, child) { + Widget titleWidget = Text(tileTitle); + if (trailingBuilder != null) { + titleWidget = Row( + children: [ + Expanded(child: titleWidget), + trailingBuilder!(context), + ], + ); + } + return ListTile( + title: titleWidget, + subtitle: AvesCaption(current.isEmpty ? noneSubtitle : current.map((v) => getName(context, v)).join(AText.separator)), + onTap: () => showSelectionDialog<List<T>>( + context: context, + builder: (context) => AvesMultiSelectionDialog<T>( + initialValue: current.toSet(), + options: Map.fromEntries(values.map((v) => MapEntry(v, getName(context, v)))), + optionSubtitleBuilder: optionSubtitleBuilder, + title: dialogTitle, + ), + onSelection: onSelection, + ), + ); + }, + ); + } +} + class SettingsDurationListTile extends StatelessWidget { final int Function(BuildContext, Settings) selector; final ValueChanged<int> onChanged; diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart index e5f9924dc..87b0fc0f3 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -1,17 +1,17 @@ import 'dart:async'; import 'package:aves/model/device.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/home_widget_settings_page.dart b/lib/widgets/settings/home_widget_settings_page.dart index 8be4d42c8..a2e49fe95 100644 --- a/lib/widgets/settings/home_widget_settings_page.dart +++ b/lib/widgets/settings/home_widget_settings_page.dart @@ -1,19 +1,18 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/enums/widget_shape.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/widget_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves/widgets/common/basic/color_indicator.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; import 'package:aves/widgets/home_widget.dart'; import 'package:aves/widgets/settings/common/collection_tile.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -218,7 +217,6 @@ class HomeWidgetOutlineSelector extends StatefulWidget { } class _HomeWidgetOutlineSelectorState extends State<HomeWidgetOutlineSelector> { - static const radius = Constants.colorPickerRadius; static const List<Color?> options = [ null, Colors.black, @@ -243,14 +241,8 @@ class _HomeWidgetOutlineSelectorState extends State<HomeWidgetOutlineSelector> { return options.map((selected) { return DropdownMenuItem<Color?>( value: selected, - child: Container( - height: radius * 2, - width: radius * 2, - decoration: BoxDecoration( - color: selected, - border: AvesBorder.border(context), - shape: BoxShape.circle, - ), + child: ColorIndicator( + value: selected, child: selected == null ? const Icon(AIcons.clear) : null, ), ); diff --git a/lib/widgets/settings/language/language.dart b/lib/widgets/settings/language/language.dart index 472c2b990..be642a582 100644 --- a/lib/widgets/settings/language/language.dart +++ b/lib/widgets/settings/language/language.dart @@ -1,17 +1,17 @@ import 'dart:async'; import 'package:aves/model/settings/enums/coordinate_format.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/ref/poi.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/language/locale_tile.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -56,7 +56,7 @@ class SettingsTileLanguageCoordinateFormat extends SettingsTile { onSelection: (v) => settings.coordinateFormat = v, tileTitle: title(context), dialogTitle: context.l10n.settingsCoordinateFormatDialogTitle, - optionSubtitleBuilder: (value) => value.format(context.l10n, Constants.pointNemo), + optionSubtitleBuilder: (value) => value.format(context.l10n, PointsOfInterest.pointNemo), ); } diff --git a/lib/widgets/settings/navigation/drawer_editor_banner.dart b/lib/widgets/settings/navigation/drawer_editor_banner.dart index bd74132bf..b05e63963 100644 --- a/lib/widgets/settings/navigation/drawer_editor_banner.dart +++ b/lib/widgets/settings/navigation/drawer_editor_banner.dart @@ -1,4 +1,5 @@ import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; @@ -11,7 +12,7 @@ class DrawerEditorBanner extends StatelessWidget { padding: const EdgeInsets.all(16), child: Row( children: [ - const Icon(AIcons.info), + const FontSizeIconTheme(child: Icon(AIcons.info)), const SizedBox(width: 16), Expanded(child: Text(context.l10n.settingsNavigationDrawerBanner)), ], diff --git a/lib/widgets/settings/navigation/drawer_tab_albums.dart b/lib/widgets/settings/navigation/drawer_tab_albums.dart index 1b62dcd2a..5ee754397 100644 --- a/lib/widgets/settings/navigation/drawer_tab_albums.dart +++ b/lib/widgets/settings/navigation/drawer_tab_albums.dart @@ -39,17 +39,17 @@ class _DrawerAlbumTabState extends State<DrawerAlbumTab> { itemBuilder: (context, index) { final album = items[index]; final filter = AlbumFilter(album, source.getAlbumDisplayName(context, album)); + void onPressed() => setState(() => items.remove(album)); return ListTile( key: ValueKey(album), leading: DrawerFilterIcon(filter: filter), title: DrawerFilterTitle(filter: filter), trailing: IconButton( icon: const Icon(AIcons.clear), - onPressed: () { - setState(() => items.remove(album)); - }, + onPressed: onPressed, tooltip: context.l10n.actionRemove, ), + onTap: settings.useTvLayout ? onPressed : null, ); }, itemCount: items.length, diff --git a/lib/widgets/settings/navigation/drawer_tab_fixed.dart b/lib/widgets/settings/navigation/drawer_tab_fixed.dart index 1069693a3..e583cb452 100644 --- a/lib/widgets/settings/navigation/drawer_tab_fixed.dart +++ b/lib/widgets/settings/navigation/drawer_tab_fixed.dart @@ -40,6 +40,16 @@ class _DrawerFixedListTabState<T> extends State<DrawerFixedListTab<T>> { itemBuilder: (context, index) { final filter = widget.items[index]; final visible = visibleItems.contains(filter); + void onPressed() { + setState(() { + if (visible) { + visibleItems.remove(filter); + } else { + visibleItems.add(filter); + } + }); + } + return Opacity( key: ValueKey(filter), opacity: visible ? 1 : .4, @@ -48,17 +58,10 @@ class _DrawerFixedListTabState<T> extends State<DrawerFixedListTab<T>> { title: widget.title(filter), trailing: IconButton( icon: Icon(visible ? AIcons.hide : AIcons.show), - onPressed: () { - setState(() { - if (visible) { - visibleItems.remove(filter); - } else { - visibleItems.add(filter); - } - }); - }, + onPressed: onPressed, tooltip: visible ? context.l10n.hideTooltip : context.l10n.showTooltip, ), + onTap: settings.useTvLayout ? onPressed : null, ), ); }, diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index 93ea8a347..e81cbd76a 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -1,16 +1,16 @@ import 'dart:async'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/navigation/confirmation_dialogs.dart'; import 'package:aves/widgets/settings/navigation/drawer.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/privacy/access_grants_page.dart b/lib/widgets/settings/privacy/access_grants_page.dart index 0edb980fc..36c0e0256 100644 --- a/lib/widgets/settings/privacy/access_grants_page.dart +++ b/lib/widgets/settings/privacy/access_grants_page.dart @@ -1,5 +1,6 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; @@ -96,7 +97,7 @@ class _Header extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ - const Icon(AIcons.info), + const FontSizeIconTheme(child: Icon(AIcons.info)), const SizedBox(width: 16), Expanded(child: Text(context.l10n.settingsStorageAccessBanner)), ], diff --git a/lib/widgets/settings/privacy/file_picker/crumb_line.dart b/lib/widgets/settings/privacy/file_picker/crumb_line.dart index a6b408101..30cdfdfa6 100644 --- a/lib/widgets/settings/privacy/file_picker/crumb_line.dart +++ b/lib/widgets/settings/privacy/file_picker/crumb_line.dart @@ -1,6 +1,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class CrumbLine extends StatefulWidget { diff --git a/lib/widgets/settings/privacy/file_picker/file_picker_page.dart b/lib/widgets/settings/privacy/file_picker/file_picker_page.dart index c41580e78..6d193c6cc 100644 --- a/lib/widgets/settings/privacy/file_picker/file_picker_page.dart +++ b/lib/widgets/settings/privacy/file_picker/file_picker_page.dart @@ -5,13 +5,15 @@ import 'package:aves/services/common/services.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/constants.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -69,7 +71,7 @@ class _FilePickerPageState extends State<FilePickerPage> { appBar: AppBar( title: Text(_getTitle(context)), actions: [ - MenuIconTheme( + FontSizeIconTheme( child: PopupMenuButton<_PickerAction>( itemBuilder: (context) { return [ @@ -182,7 +184,7 @@ class _FilePickerPageState extends State<FilePickerPage> { Widget _buildContentLine(BuildContext context, FileSystemEntity content) { return ListTile( leading: const Icon(AIcons.folder), - title: Text('${Constants.fsi}${pContext.split(content.path).last}${Constants.pdi}'), + title: Text('${Unicode.FSI}${pContext.split(content.path).last}${Unicode.PDI}'), onTap: () { _goTo(content.path); setState(() {}); @@ -191,7 +193,7 @@ class _FilePickerPageState extends State<FilePickerPage> { } void _goTo(String path) { - _directory = VolumeRelativeDirectory.fromPath(path)!; + _directory = androidFileUtils.relativeDirectoryFromPath(path)!; _contents = null; final contents = <Directory>[]; Directory(currentDirectoryPath).list().listen((event) { diff --git a/lib/widgets/settings/privacy/hidden_items_page.dart b/lib/widgets/settings/privacy/hidden_items_page.dart index 62a63b537..0e94d6372 100644 --- a/lib/widgets/settings/privacy/hidden_items_page.dart +++ b/lib/widgets/settings/privacy/hidden_items_page.dart @@ -3,6 +3,7 @@ import 'package:aves/model/filters/path.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/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -125,17 +126,19 @@ class _HiddenPaths extends StatelessWidget { child: ListView( shrinkWrap: true, children: [ - ...pathList.map((pathFilter) => ListTile( - title: Text(pathFilter.path), - dense: true, - trailing: IconButton( - icon: const Icon(AIcons.clear), - onPressed: () { - settings.changeFilterVisibility({pathFilter}, true); - }, - tooltip: context.l10n.actionRemove, - ), - )), + ...pathList.map((pathFilter) { + void onPressed() => settings.changeFilterVisibility({pathFilter}, true); + return ListTile( + title: Text(pathFilter.path), + dense: true, + trailing: IconButton( + icon: const Icon(AIcons.clear), + onPressed: onPressed, + tooltip: context.l10n.actionRemove, + ), + onTap: settings.useTvLayout ? onPressed : null, + ); + }), ], ), ), @@ -176,7 +179,7 @@ class _Banner extends StatelessWidget { padding: const EdgeInsets.all(16), child: Row( children: [ - const Icon(AIcons.info), + const FontSizeIconTheme(child: Icon(AIcons.info)), const SizedBox(width: 16), Expanded(child: Text(bannerText)), ], diff --git a/lib/widgets/settings/screen_saver_settings_page.dart b/lib/widgets/settings/screen_saver_settings_page.dart index aed3abf0f..ca356f93d 100644 --- a/lib/widgets/settings/screen_saver_settings_page.dart +++ b/lib/widgets/settings/screen_saver_settings_page.dart @@ -1,11 +1,11 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/collection_tile.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/settings_mobile_page.dart b/lib/widgets/settings/settings_mobile_page.dart new file mode 100644 index 000000000..2950b0148 --- /dev/null +++ b/lib/widgets/settings/settings_mobile_page.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/ref/mime_types.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/popup/menu_row.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/search/route.dart'; +import 'package:aves/widgets/settings/app_export/items.dart'; +import 'package:aves/widgets/settings/app_export/selection_dialog.dart'; +import 'package:aves/widgets/settings/settings_page.dart'; +import 'package:aves/widgets/settings/settings_search.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class SettingsMobilePage extends StatefulWidget { + const SettingsMobilePage({super.key}); + + @override + State<SettingsMobilePage> createState() => _SettingsMobilePageState(); +} + +class _SettingsMobilePageState extends State<SettingsMobilePage> with FeedbackMixin { + final ValueNotifier<String?> _expandedNotifier = ValueNotifier(null); + + @override + void dispose() { + _expandedNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AvesScaffold( + appBar: AppBar( + title: InteractiveAppBarTitle( + onTap: () => _goToSearch(context), + child: Text(context.l10n.settingsPageTitle), + ), + actions: [ + IconButton( + icon: const Icon(AIcons.search), + onPressed: () => _goToSearch(context), + tooltip: MaterialLocalizations.of(context).searchFieldLabel, + ), + PopupMenuButton<SettingsAction>( + itemBuilder: (context) { + return [ + PopupMenuItem( + value: SettingsAction.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.fileImport)), + ), + ]; + }, + onSelected: (action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + _onActionSelected(action); + }, + ), + ].map((v) => FontSizeIconTheme(child: v)).toList(), + ), + body: GestureAreaProtectorStack( + child: SafeArea( + bottom: false, + child: AnimationLimiter( + child: SettingsListView( + children: SettingsPage.sections.map((v) => v.build(context, _expandedNotifier)).toList(), + ), + ), + ), + ), + ); + } + + static const String exportVersionKey = 'version'; + static const int exportVersion = 1; + + void _onActionSelected(SettingsAction action) async { + final source = context.read<CollectionSource>(); + switch (action) { + case SettingsAction.export: + final toExport = await showDialog<Set<AppExportItem>>( + context: context, + builder: (context) => AppExportItemSelectionDialog( + title: context.l10n.settingsActionExportDialogTitle, + ), + ); + if (toExport == null || toExport.isEmpty) return; + + final allMap = Map.fromEntries(toExport.map((v) { + final jsonMap = v.export(source); + return jsonMap != null ? MapEntry(v.name, jsonMap) : null; + }).whereNotNull()); + allMap[exportVersionKey] = exportVersion; + final allJsonString = jsonEncode(allMap); + + final success = await storageService.createFile( + 'aves-settings-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.json', + MimeTypes.json, + Uint8List.fromList(utf8.encode(allJsonString)), + ); + if (success != null) { + if (success) { + showFeedback(context, context.l10n.genericSuccessFeedback); + } else { + showFeedback(context, context.l10n.genericFailureFeedback); + } + } + break; + case SettingsAction.import: + // specifying the JSON MIME type to restrict openable files is correct in theory, + // but older devices (e.g. SM-P580, API 27) that do not recognize JSON files as such would filter them out + final bytes = await storageService.openFile(); + if (bytes.isNotEmpty) { + try { + final allJsonString = utf8.decode(bytes); + final allJsonMap = jsonDecode(allJsonString); + + final version = allJsonMap[exportVersionKey]; + final importable = <AppExportItem, dynamic>{}; + if (version == null) { + // backward compatibility before versioning + importable[AppExportItem.settings] = allJsonMap; + } else { + if (allJsonMap is! Map) { + debugPrint('failed to import app json=$allJsonMap'); + showFeedback(context, context.l10n.genericFailureFeedback); + return; + } + allJsonMap.keys.where((v) => v != exportVersionKey).forEach((k) { + try { + importable[AppExportItem.values.byName(k)] = allJsonMap[k]; + } catch (error, stack) { + debugPrint('failed to identify import app item=$k with error=$error\n$stack'); + } + }); + } + + final toImport = await showDialog<Set<AppExportItem>>( + context: context, + builder: (context) => AppExportItemSelectionDialog( + title: context.l10n.settingsActionImportDialogTitle, + selectableItems: importable.keys.toSet(), + ), + ); + if (toImport == null || toImport.isEmpty) return; + + await Future.forEach<AppExportItem>(toImport, (item) async { + return item.import(importable[item], source); + }); + showFeedback(context, context.l10n.genericSuccessFeedback); + } catch (error) { + debugPrint('failed to import app json, error=$error'); + showFeedback(context, context.l10n.genericFailureFeedback); + } + } + break; + } + } + + void _goToSearch(BuildContext context) { + Navigator.maybeOf(context)?.push( + SearchPageRoute( + delegate: SettingsSearchDelegate( + searchFieldLabel: context.l10n.settingsSearchFieldLabel, + sections: SettingsPage.sections, + ), + ), + ); + } +} diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index c917ffcde..f5eb8e035 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -1,57 +1,27 @@ -import 'dart:convert'; import 'dart:math'; -import 'dart:typed_data'; -import 'package:aves/model/actions/settings_actions.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/ref/mime_types.dart'; -import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/action_mixins/feedback.dart'; -import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; -import 'package:aves/widgets/common/basic/insets.dart'; -import 'package:aves/widgets/common/basic/popup/menu_row.dart'; -import 'package:aves/widgets/common/basic/scaffold.dart'; -import 'package:aves/widgets/common/behaviour/pop/scope.dart'; -import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; -import 'package:aves/widgets/common/search/route.dart'; -import 'package:aves/widgets/navigation/tv_rail.dart'; import 'package:aves/widgets/settings/accessibility/accessibility.dart'; -import 'package:aves/widgets/settings/app_export/items.dart'; -import 'package:aves/widgets/settings/app_export/selection_dialog.dart'; import 'package:aves/widgets/settings/display/display.dart'; import 'package:aves/widgets/settings/language/language.dart'; import 'package:aves/widgets/settings/navigation/navigation.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; -import 'package:aves/widgets/settings/settings_search.dart'; +import 'package:aves/widgets/settings/settings_mobile_page.dart'; +import 'package:aves/widgets/settings/settings_tv_page.dart'; import 'package:aves/widgets/settings/thumbnails/thumbnails.dart'; import 'package:aves/widgets/settings/video/video.dart'; import 'package:aves/widgets/settings/viewer/viewer.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -class SettingsPage extends StatefulWidget { +class SettingsPage extends StatelessWidget { static const routeName = '/settings'; - const SettingsPage({super.key}); - - @override - State<SettingsPage> createState() => _SettingsPageState(); -} - -class _SettingsPageState extends State<SettingsPage> with FeedbackMixin { - final ValueNotifier<String?> _expandedNotifier = ValueNotifier(null); - final ValueNotifier<int> _tvSelectedIndexNotifier = ValueNotifier(0); - static final List<SettingsSection> sections = [ NavigationSection(), ThumbnailsSection(), @@ -63,209 +33,22 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin { LanguageSection(), ]; - @override - void dispose() { - _expandedNotifier.dispose(); - _tvSelectedIndexNotifier.dispose(); - super.dispose(); - } + const SettingsPage({super.key}); @override Widget build(BuildContext context) { - final appBarTitle = Text(context.l10n.settingsPageTitle); - if (settings.useTvLayout) { - return AvesScaffold( - body: AvesPopScope( - handlers: const [TvNavigationPopHandler.pop], - child: Row( - children: [ - TvRail( - controller: context.read<TvRailController>(), - ), - Expanded( - child: Column( - children: [ - const SizedBox(height: 8), - DirectionalSafeArea( - start: false, - bottom: false, - child: AppBar( - automaticallyImplyLeading: false, - title: appBarTitle, - elevation: 0, - primary: false, - ), - ), - Expanded( - child: MediaQuery.removePadding( - context: context, - removeLeft: true, - removeTop: true, - removeRight: true, - removeBottom: true, - child: const _TvRail(), - ), - ), - ], - ), - ), - ], - ), - ), - ); + return const SettingsTvPage(); } else { - return AvesScaffold( - appBar: AppBar( - title: InteractiveAppBarTitle( - onTap: () => _goToSearch(context), - child: appBarTitle, - ), - actions: [ - IconButton( - icon: const Icon(AIcons.search), - onPressed: () => _goToSearch(context), - tooltip: MaterialLocalizations.of(context).searchFieldLabel, - ), - MenuIconTheme( - child: PopupMenuButton<SettingsAction>( - itemBuilder: (context) { - return [ - PopupMenuItem( - value: SettingsAction.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.fileImport)), - ), - ]; - }, - onSelected: (action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - _onActionSelected(action); - }, - ), - ), - ], - ), - body: GestureAreaProtectorStack( - child: SafeArea( - bottom: false, - child: AnimationLimiter( - child: _SettingsListView( - children: sections.map((v) => v.build(context, _expandedNotifier)).toList(), - ), - ), - ), - ), - ); + return const SettingsMobilePage(); } } - - static const String exportVersionKey = 'version'; - static const int exportVersion = 1; - - void _onActionSelected(SettingsAction action) async { - final source = context.read<CollectionSource>(); - switch (action) { - case SettingsAction.export: - final toExport = await showDialog<Set<AppExportItem>>( - context: context, - builder: (context) => AppExportItemSelectionDialog( - title: context.l10n.settingsActionExportDialogTitle, - ), - ); - if (toExport == null || toExport.isEmpty) return; - - final allMap = Map.fromEntries(toExport.map((v) { - final jsonMap = v.export(source); - return jsonMap != null ? MapEntry(v.name, jsonMap) : null; - }).whereNotNull()); - allMap[exportVersionKey] = exportVersion; - final allJsonString = jsonEncode(allMap); - - final success = await storageService.createFile( - 'aves-settings-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.json', - MimeTypes.json, - Uint8List.fromList(utf8.encode(allJsonString)), - ); - if (success != null) { - if (success) { - showFeedback(context, context.l10n.genericSuccessFeedback); - } else { - showFeedback(context, context.l10n.genericFailureFeedback); - } - } - break; - case SettingsAction.import: - // specifying the JSON MIME type to restrict openable files is correct in theory, - // but older devices (e.g. SM-P580, API 27) that do not recognize JSON files as such would filter them out - final bytes = await storageService.openFile(); - if (bytes.isNotEmpty) { - try { - final allJsonString = utf8.decode(bytes); - final allJsonMap = jsonDecode(allJsonString); - - final version = allJsonMap[exportVersionKey]; - final importable = <AppExportItem, dynamic>{}; - if (version == null) { - // backward compatibility before versioning - importable[AppExportItem.settings] = allJsonMap; - } else { - if (allJsonMap is! Map) { - debugPrint('failed to import app json=$allJsonMap'); - showFeedback(context, context.l10n.genericFailureFeedback); - return; - } - allJsonMap.keys.where((v) => v != exportVersionKey).forEach((k) { - try { - importable[AppExportItem.values.byName(k)] = allJsonMap[k]; - } catch (error, stack) { - debugPrint('failed to identify import app item=$k with error=$error\n$stack'); - } - }); - } - - final toImport = await showDialog<Set<AppExportItem>>( - context: context, - builder: (context) => AppExportItemSelectionDialog( - title: context.l10n.settingsActionImportDialogTitle, - selectableItems: importable.keys.toSet(), - ), - ); - if (toImport == null || toImport.isEmpty) return; - - await Future.forEach<AppExportItem>(toImport, (item) async { - return item.import(importable[item], source); - }); - showFeedback(context, context.l10n.genericSuccessFeedback); - } catch (error) { - debugPrint('failed to import app json, error=$error'); - showFeedback(context, context.l10n.genericFailureFeedback); - } - } - break; - } - } - - void _goToSearch(BuildContext context) { - Navigator.maybeOf(context)?.push( - SearchPageRoute( - delegate: SettingsSearchDelegate( - searchFieldLabel: context.l10n.settingsSearchFieldLabel, - sections: sections, - ), - ), - ); - } } -class _SettingsListView extends StatelessWidget { +class SettingsListView extends StatelessWidget { final List<Widget> children; - const _SettingsListView({ + const SettingsListView({ super.key, required this.children, }); @@ -303,90 +86,3 @@ class _SettingsListView extends StatelessWidget { ); } } - -class _SettingsSectionBody extends StatelessWidget { - final Future<List<SettingsTile>> loader; - - const _SettingsSectionBody({required this.loader}); - - @override - Widget build(BuildContext context) { - return FutureBuilder<List<SettingsTile>>( - future: loader, - builder: (context, snapshot) { - final tiles = snapshot.data; - if (tiles == null) return const SizedBox(); - - return _SettingsListView( - key: ValueKey(loader), - children: tiles.map((v) => v.build(context)).toList(), - ); - }, - ); - } -} - -class _TvRail extends StatefulWidget { - const _TvRail(); - - @override - State<_TvRail> createState() => _TvRailState(); -} - -class _TvRailState extends State<_TvRail> { - final ValueNotifier<int> _indexNotifier = ValueNotifier(0); - - @override - void dispose() { - _indexNotifier.dispose(); - super.dispose(); - } - - static final List<SettingsSection> sections = _SettingsPageState.sections; - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder<int>( - valueListenable: _indexNotifier, - builder: (context, selectedIndex, child) { - final rail = NavigationRail( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - extended: true, - destinations: sections - .map((section) => NavigationRailDestination( - icon: section.icon(context), - label: Text(section.title(context)), - )) - .toList(), - selectedIndex: selectedIndex, - onDestinationSelected: (index) => _indexNotifier.value = index, - minExtendedWidth: TvRail.minExtendedWidth, - ); - return LayoutBuilder( - builder: (context, constraints) { - return Row( - children: [ - SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight(child: rail), - ), - ), - Expanded( - child: MediaQuery.removePadding( - context: context, - removeLeft: !context.isRtl, - removeRight: context.isRtl, - child: _SettingsSectionBody( - loader: Future.value(sections[selectedIndex].tiles(context)), - ), - ), - ), - ], - ); - }, - ); - }, - ); - } -} diff --git a/lib/widgets/settings/settings_tv_page.dart b/lib/widgets/settings/settings_tv_page.dart new file mode 100644 index 000000000..1fc742bb8 --- /dev/null +++ b/lib/widgets/settings/settings_tv_page.dart @@ -0,0 +1,144 @@ +import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; +import 'package:aves/widgets/common/behaviour/pop/scope.dart'; +import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/navigation/tv_rail.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves/widgets/settings/settings_page.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SettingsTvPage extends StatelessWidget { + const SettingsTvPage({super.key}); + + @override + Widget build(BuildContext context) { + return AvesScaffold( + body: AvesPopScope( + handlers: const [TvNavigationPopHandler.pop], + child: Row( + children: [ + TvRail( + controller: context.read<TvRailController>(), + ), + Expanded( + child: Column( + children: [ + const SizedBox(height: 8), + DirectionalSafeArea( + start: false, + bottom: false, + child: AppBar( + automaticallyImplyLeading: false, + title: Text(context.l10n.settingsPageTitle), + elevation: 0, + primary: false, + ), + ), + Expanded( + child: MediaQuery.removePadding( + context: context, + removeLeft: true, + removeTop: true, + removeRight: true, + removeBottom: true, + child: const _Content(), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _Content extends StatefulWidget { + const _Content(); + + @override + State<_Content> createState() => _ContentState(); +} + +class _ContentState extends State<_Content> { + final ValueNotifier<int> _indexNotifier = ValueNotifier(0); + + @override + void dispose() { + _indexNotifier.dispose(); + super.dispose(); + } + + static final List<SettingsSection> sections = SettingsPage.sections; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder<int>( + valueListenable: _indexNotifier, + builder: (context, selectedIndex, child) { + final rail = NavigationRail( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + extended: true, + destinations: sections + .map((section) => NavigationRailDestination( + icon: section.icon(context), + label: Text(section.title(context)), + )) + .toList(), + selectedIndex: selectedIndex, + onDestinationSelected: (index) => _indexNotifier.value = index, + minExtendedWidth: TvRail.minExtendedWidth, + ); + return LayoutBuilder( + builder: (context, constraints) { + return Row( + children: [ + SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight(child: rail), + ), + ), + Expanded( + child: MediaQuery.removePadding( + context: context, + removeLeft: !context.isRtl, + removeRight: context.isRtl, + child: _Section( + loader: Future.value(sections[selectedIndex].tiles(context)), + ), + ), + ), + ], + ); + }, + ); + }, + ); + } +} + +class _Section extends StatelessWidget { + final Future<List<SettingsTile>> loader; + + const _Section({required this.loader}); + + @override + Widget build(BuildContext context) { + return FutureBuilder<List<SettingsTile>>( + future: loader, + builder: (context, snapshot) { + final tiles = snapshot.data; + if (tiles == null) return const SizedBox(); + + return SettingsListView( + key: ValueKey(loader), + children: tiles.map((v) => v.build(context)).toList(), + ); + }, + ); + } +} diff --git a/lib/widgets/settings/thumbnails/collection_actions_editor_page.dart b/lib/widgets/settings/thumbnails/collection_actions_editor_page.dart index 82e7495b2..551612110 100644 --- a/lib/widgets/settings/thumbnails/collection_actions_editor_page.dart +++ b/lib/widgets/settings/thumbnails/collection_actions_editor_page.dart @@ -1,8 +1,9 @@ -import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; diff --git a/lib/widgets/settings/thumbnails/overlay.dart b/lib/widgets/settings/thumbnails/overlay.dart index aa1bef264..3f8be2e1d 100644 --- a/lib/widgets/settings/thumbnails/overlay.dart +++ b/lib/widgets/settings/thumbnails/overlay.dart @@ -1,16 +1,14 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; -import 'package:aves/model/settings/enums/thumbnail_overlay_location_icon.dart'; -import 'package:aves/model/settings/enums/thumbnail_overlay_tag_icon.dart'; 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/view/view.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index 2bf00f4e2..3edac6e01 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -1,4 +1,5 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/ref/bursts.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -27,6 +28,7 @@ class ThumbnailsSection extends SettingsSection { List<SettingsTile> tiles(BuildContext context) => [ if (!settings.useTvLayout) SettingsTileCollectionQuickActions(), SettingsTileThumbnailOverlay(), + SettingsTileBurstPatterns(), ]; } @@ -53,3 +55,19 @@ class SettingsTileThumbnailOverlay extends SettingsTile { builder: (context) => const ThumbnailOverlayPage(), ); } + +class SettingsTileBurstPatterns extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsCollectionBurstPatternsTile; + + @override + Widget build(BuildContext context) => SettingsMultiSelectionListTile<String>( + values: BurstPatterns.options, + getName: (context, v) => BurstPatterns.getName(v), + selector: (context, s) => s.collectionBurstPatterns, + onSelection: (v) => settings.collectionBurstPatterns = v, + tileTitle: title(context), + noneSubtitle: context.l10n.settingsCollectionBurstPatternsNone, + optionSubtitleBuilder: BurstPatterns.getExample, + ); +} diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart index e8ee34c9c..509284435 100644 --- a/lib/widgets/settings/video/controls.dart +++ b/lib/widgets/settings/video/controls.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class VideoControlsPage extends StatelessWidget { diff --git a/lib/widgets/settings/video/subtitle_sample.dart b/lib/widgets/settings/video/subtitle_sample.dart index c8ae248e4..40879e56a 100644 --- a/lib/widgets/settings/video/subtitle_sample.dart +++ b/lib/widgets/settings/video/subtitle_sample.dart @@ -1,11 +1,11 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/widgets/common/basic/text/background_painter.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -35,7 +35,7 @@ class SubtitleSample extends StatelessWidget { gradient: const LinearGradient( begin: Alignment.bottomLeft, end: Alignment.topRight, - colors: Constants.boraBoraGradientColors, + colors: AColors.boraBoraGradient, ), border: AvesBorder.border(context), borderRadius: const BorderRadius.all(Radius.circular(24)), diff --git a/lib/widgets/settings/video/subtitle_theme.dart b/lib/widgets/settings/video/subtitle_theme.dart index 6fc3fd7e2..4d7857736 100644 --- a/lib/widgets/settings/video/subtitle_theme.dart +++ b/lib/widgets/settings/video/subtitle_theme.dart @@ -1,12 +1,12 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/list_tiles/color.dart'; import 'package:aves/widgets/common/basic/list_tiles/slider.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/video/subtitle_sample.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index ba68e3d6b..0ac8dae08 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -2,17 +2,17 @@ import 'dart:async'; import 'package:aves/model/device.dart'; import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:aves/widgets/settings/video/controls.dart'; import 'package:aves/widgets/settings/video/subtitle_theme.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/viewer/entry_background.dart b/lib/widgets/settings/viewer/entry_background.dart index 9dd6f826c..53a3a0b4f 100644 --- a/lib/widgets/settings/viewer/entry_background.dart +++ b/lib/widgets/settings/viewer/entry_background.dart @@ -1,8 +1,7 @@ import 'package:aves/model/settings/enums/entry_background.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/common/basic/color_indicator.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class EntryBackgroundSelector extends StatefulWidget { @@ -20,8 +19,6 @@ class EntryBackgroundSelector extends StatefulWidget { } class _EntryBackgroundSelectorState extends State<EntryBackgroundSelector> { - static const radius = Constants.colorPickerRadius; - @override Widget build(BuildContext context) { return DropdownButtonHideUnderline( @@ -46,19 +43,13 @@ class _EntryBackgroundSelectorState extends State<EntryBackgroundSelector> { ].map((selected) { return DropdownMenuItem<EntryBackground>( value: selected, - child: Container( - height: radius * 2, - width: radius * 2, - decoration: BoxDecoration( - color: selected.isColor ? selected.color : null, - border: AvesBorder.border(context), - shape: BoxShape.circle, - ), + child: ColorIndicator( + value: selected.isColor ? selected.color : null, child: selected == EntryBackground.checkered ? ClipOval( child: CustomPaint( painter: CheckeredPainter( - checkSize: radius, + checkSize: ColorIndicator.radius, ), ), ) diff --git a/lib/widgets/settings/viewer/slideshow.dart b/lib/widgets/settings/viewer/slideshow.dart index f84cab854..7a9294d92 100644 --- a/lib/widgets/settings/viewer/slideshow.dart +++ b/lib/widgets/settings/viewer/slideshow.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; class ViewerSlideshowPage extends StatelessWidget { diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index 1f9707568..a9fdb2a4b 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; @@ -13,6 +12,7 @@ import 'package:aves/widgets/settings/viewer/entry_background.dart'; import 'package:aves/widgets/settings/viewer/overlay.dart'; import 'package:aves/widgets/settings/viewer/slideshow.dart'; import 'package:aves/widgets/settings/viewer/viewer_actions_editor.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index 98e58fa27..912c13173 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -1,7 +1,9 @@ -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class ViewerActionEditorPage extends StatelessWidget { @@ -9,7 +11,7 @@ class ViewerActionEditorPage extends StatelessWidget { const ViewerActionEditorPage({super.key}); - static const allAvailableActions = [ + static final allAvailableActions = [ [ EntryAction.share, EntryAction.edit, @@ -26,10 +28,7 @@ class ViewerActionEditorPage extends StatelessWidget { ], [ ...EntryActions.exportInternal, - EntryAction.videoCaptureFrame, - EntryAction.videoToggleMute, - EntryAction.videoSetSpeed, - EntryAction.videoSelectStreams, + ...EntryActions.video.whereNot((v) => v == EntryAction.videoSettings), ], EntryActions.commonMetadataActions, ]; diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 145d54c79..c6fe71c41 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -1,10 +1,10 @@ import 'package:aves/model/filters/filters.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/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/stats/percent_text.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; diff --git a/lib/widgets/stats/percent_text.dart b/lib/widgets/stats/percent_text.dart index 44681e02e..1ead911d5 100644 --- a/lib/widgets/stats/percent_text.dart +++ b/lib/widgets/stats/percent_text.dart @@ -1,4 +1,4 @@ -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -20,7 +20,7 @@ class LinearPercentIndicatorText extends StatelessWidget { TextSpan( text: percentFormat.format(percent), style: TextStyle( - shadows: theme.brightness == Brightness.dark ? Constants.embossShadows : null, + shadows: theme.brightness == Brightness.dark ? AStyles.embossShadows : null, ), ) ], diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 1a6ff3ff6..a7f6cdb77 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -12,7 +12,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.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/vault_aware.dart'; @@ -54,7 +54,8 @@ class StatsPage extends StatefulWidget { } class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMixin { - final Map<String, int> _entryCountPerCountry = {}, _entryCountPerPlace = {}, _entryCountPerTag = {}, _entryCountPerAlbum = {}; + final Map<String, int> _entryCountPerCountry = {}, _entryCountPerState = {}, _entryCountPerPlace = {}; + final Map<String, int> _entryCountPerTag = {}, _entryCountPerAlbum = {}; final Map<int, int> _entryCountPerRating = Map.fromEntries(List.generate(7, (i) => MapEntry(5 - i, 0))); late final ValueNotifier<bool> _isPageAnimatingNotifier; @@ -78,6 +79,11 @@ class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMix country += '${LocationFilter.locationSeparator}${address.countryCode}'; _entryCountPerCountry[country] = (_entryCountPerCountry[country] ?? 0) + 1; } + var state = address.stateName; + if (state != null && state.isNotEmpty) { + state += '${LocationFilter.locationSeparator}${address.stateCode}'; + _entryCountPerState[state] = (_entryCountPerState[state] ?? 0) + 1; + } final place = address.place; if (place != null && place.isNotEmpty) { _entryCountPerPlace[place] = (_entryCountPerPlace[place] ?? 0) + 1; @@ -210,6 +216,7 @@ class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMix ), locationIndicator, ..._buildFilterSection<String>(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)), + ..._buildFilterSection<String>(context, l10n.statsTopStatesSectionTitle, _entryCountPerState, (v) => LocationFilter(LocationLevel.state, v)), ..._buildFilterSection<String>(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)), ..._buildFilterSection<String>(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new), ..._buildFilterSection<String>(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => AlbumFilter(v, source.getAlbumDisplayName(context, v))), @@ -276,20 +283,32 @@ class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMix : null; Widget header = Text( title, - style: Constants.knownTitleTextStyle, + style: AStyles.knownTitleText, ); if (settings.useTvLayout) { + header = Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + header, + const SizedBox(width: 16), + Icon(AIcons.next, color: hasMore ? null : Theme.of(context).disabledColor), + ], + ), + ); header = Container( padding: const EdgeInsets.symmetric(vertical: 12), alignment: AlignmentDirectional.centerStart, - child: InkWell( - onTap: onHeaderPressed, - borderRadius: const BorderRadius.all(Radius.circular(123)), - child: Padding( - padding: const EdgeInsets.all(16), - child: header, - ), - ), + // prevent ink response when tapping the header does nothing, + // because otherwise Play Store reviewers think it is broken navigation + child: onHeaderPressed != null + ? InkWell( + onTap: onHeaderPressed, + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: header, + ) + : Focus(child: header), ); } else { header = Padding( diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index ee5ed6e72..3f8812c78 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -2,9 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/actions/move_type.dart'; -import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/favourites.dart'; @@ -13,7 +10,6 @@ import 'package:aves/model/entry/extensions/metadata_edition.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/filters/filters.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'; @@ -38,6 +34,7 @@ import 'package:aves/widgets/viewer/debug/debug_page.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -92,6 +89,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix return targetEntry.isSvg; case EntryAction.videoCaptureFrame: return canWrite && targetEntry.isVideo; + case EntryAction.lockViewer: case EntryAction.videoToggleMute: return !settings.useTvLayout && targetEntry.isVideo; case EntryAction.videoSelectStreams: @@ -188,7 +186,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix _addShortcut(context, targetEntry); break; case EntryAction.copyToClipboard: - androidAppService.copyToClipboard(targetEntry.uri, targetEntry.bestTitle).then((success) { + appService.copyToClipboard(targetEntry.uri, targetEntry.bestTitle).then((success) { showFeedback(context, success ? context.l10n.genericSuccessFeedback : context.l10n.genericFailureFeedback); }); break; @@ -214,7 +212,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix _move(context, targetEntry, moveType: MoveType.move); break; case EntryAction.share: - androidAppService.shareEntries({targetEntry}).then((success) { + appService.shareEntries({targetEntry}).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; @@ -235,6 +233,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.viewSource: _goToSourceViewer(context, targetEntry); break; + case EntryAction.lockViewer: + const LockViewNotification(locked: true).dispatch(context); + break; // video case EntryAction.videoCaptureFrame: case EntryAction.videoToggleMute: @@ -255,22 +256,22 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } break; case EntryAction.edit: - androidAppService.edit(targetEntry.uri, targetEntry.mimeType).then((success) { + appService.edit(targetEntry.uri, targetEntry.mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; case EntryAction.open: - androidAppService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) { + appService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; case EntryAction.openMap: - androidAppService.openMap(targetEntry.latLng!).then((success) { + appService.openMap(targetEntry.latLng!).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; case EntryAction.setAs: - androidAppService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) { + appService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; @@ -334,7 +335,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final uri = fields['uri'] as String?; final mimeType = fields['mimeType'] as String?; if (uri != null && mimeType != null) { - await androidAppService.shareSingle(uri, mimeType).then((success) { + await appService.shareSingle(uri, mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); }); } @@ -363,7 +364,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final name = result.item2; if (name.isEmpty) return; - await androidAppService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri); + await appService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri); if (!device.showPinShortcutFeedback) { showFeedback(context, context.l10n.genericSuccessFeedback); } diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 62eeffbd0..33bb488e0 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -2,13 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/actions/events.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/info.dart'; import 'package:aves/model/entry/extensions/metadata_edition.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; +import 'package:aves/model/events.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/geotiff.dart'; import 'package:aves/model/settings/settings.dart'; @@ -24,6 +23,7 @@ import 'package:aves/widgets/map/map_page.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/info/embedded/notifications.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/viewer/action/single_entry_editor.dart b/lib/widgets/viewer/action/single_entry_editor.dart index bee999ece..dd8d3e538 100644 --- a/lib/widgets/viewer/action/single_entry_editor.dart +++ b/lib/widgets/viewer/action/single_entry_editor.dart @@ -34,6 +34,7 @@ mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin { if (isMainMode && source != null) { Set<String> obsoleteTags = targetEntry.tags; String? obsoleteCountryCode = targetEntry.addressDetails?.countryCode; + String? obsoleteStateCode = targetEntry.addressDetails?.stateCode; await source.refreshEntries({targetEntry}, dataTypes); @@ -43,6 +44,9 @@ mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin { if (obsoleteCountryCode != null) { source.invalidateCountryFilterSummary(countryCodes: {obsoleteCountryCode}); } + if (obsoleteStateCode != null) { + source.invalidateStateFilterSummary(stateCodes: {obsoleteStateCode}); + } if (obsoleteTags.isNotEmpty) { source.invalidateTagFilterSummary(tags: obsoleteTags); } diff --git a/lib/widgets/viewer/action/video_action_delegate.dart b/lib/widgets/viewer/action/video_action_delegate.dart index df67b5a91..bff1f5412 100644 --- a/lib/widgets/viewer/action/video_action_delegate.dart +++ b/lib/widgets/viewer/action/video_action_delegate.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/props.dart'; @@ -10,7 +9,6 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; @@ -21,6 +19,7 @@ import 'package:aves/widgets/dialogs/video_speed_dialog.dart'; import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart'; import 'package:aves/widgets/settings/video/video_settings_page.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -69,7 +68,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix await controller.seekTo(controller.currentPosition + 10000); break; case EntryAction.openVideo: - await androidAppService.open(entry.uri, entry.mimeTypeAnySubtype, forceChooser: false).then((success) { + await appService.open(entry.uri, entry.mimeTypeAnySubtype, forceChooser: false).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; @@ -112,16 +111,17 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final l10n = context.l10n; if (success) { final _collection = collection; + // get navigator beforehand because + // local context may be deactivated when action is triggered after navigation + final navigator = Navigator.maybeOf(context); final showAction = _collection != null ? SnackBarAction( label: l10n.showButtonLabel, onPressed: () { - // local context may be deactivated when action is triggered after navigation - final context = AvesApp.navigatorKey.currentContext; - if (context != null) { + if (navigator != null) { final source = _collection.source; final newUri = newFields['uri'] as String?; - Navigator.maybeOf(context)?.pushAndRemoveUntil( + navigator.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( diff --git a/lib/widgets/viewer/controls/controller.dart b/lib/widgets/viewer/controls/controller.dart index 5808a11ee..7c087f615 100644 --- a/lib/widgets/viewer/controls/controller.dart +++ b/lib/widgets/viewer/controls/controller.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/model/entry/entry.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/viewer/controls/events.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; class ViewerController { diff --git a/lib/widgets/viewer/controls/intents.dart b/lib/widgets/viewer/controls/intents.dart index f59918194..66c76dea8 100644 --- a/lib/widgets/viewer/controls/intents.dart +++ b/lib/widgets/viewer/controls/intents.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/widgets.dart'; class ShowPreviousIntent extends Intent { diff --git a/lib/widgets/viewer/controls/notifications.dart b/lib/widgets/viewer/controls/notifications.dart index 5cb76faa7..1d65991e9 100644 --- a/lib/widgets/viewer/controls/notifications.dart +++ b/lib/widgets/viewer/controls/notifications.dart @@ -1,11 +1,17 @@ -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; +@immutable +class LockViewNotification extends Notification { + final bool locked; + + const LockViewNotification({required this.locked}); +} + @immutable class PopVisualNotification extends Notification {} diff --git a/lib/widgets/viewer/controls/shortcuts.dart b/lib/widgets/viewer/controls/shortcuts.dart index af2c9b9b8..25c0b0abe 100644 --- a/lib/widgets/viewer/controls/shortcuts.dart +++ b/lib/widgets/viewer/controls/shortcuts.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/widgets/viewer/controls/intents.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/widgets/viewer/debug/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart index a45b213ed..a987e9a6b 100644 --- a/lib/widgets/viewer/debug/debug_page.dart +++ b/lib/widgets/viewer/debug/debug_page.dart @@ -1,8 +1,8 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/favourites.dart'; -import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/images.dart'; +import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/theme/icons.dart'; @@ -130,7 +130,6 @@ class ViewerDebugPage extends StatelessWidget { 'sizeBytes': '${entry.sizeBytes}', 'isFavourite': '${entry.isFavourite}', 'isSvg': '${entry.isSvg}', - 'isPhoto': '${entry.isPhoto}', 'isVideo': '${entry.isVideo}', 'isCatalogued': '${entry.isCatalogued}', 'isAnimated': '${entry.isAnimated}', diff --git a/lib/widgets/viewer/debug/metadata.dart b/lib/widgets/viewer/debug/metadata.dart index 99e403ccc..ccf4eb7d5 100644 --- a/lib/widgets/viewer/debug/metadata.dart +++ b/lib/widgets/viewer/debug/metadata.dart @@ -2,9 +2,11 @@ import 'dart:collection'; import 'dart:typed_data'; import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/location.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/android_debug_service.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/services/geocoding_service.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; @@ -23,7 +25,7 @@ class MetadataTab extends StatefulWidget { class _MetadataTabState extends State<MetadataTab> { late Future<Map> _bitmapFactoryLoader, _contentResolverMetadataLoader, _exifInterfaceMetadataLoader; - late Future<Map> _mediaMetadataLoader, _metadataExtractorLoader, _pixyMetaLoader, _tiffStructureLoader; + late Future<Map> _mediaMetadataLoader, _metadataExtractorLoader, _pixyMetaLoader, _tiffStructureLoader, _addressLoader; late Future<String?> _mp4ParserDumpLoader; // MediaStore timestamp keys @@ -47,6 +49,27 @@ class _MetadataTabState extends State<MetadataTab> { _mp4ParserDumpLoader = AndroidDebugService.getMp4ParserDump(entry); _pixyMetaLoader = AndroidDebugService.getPixyMetadata(entry); _tiffStructureLoader = AndroidDebugService.getTiffStructure(entry); + _addressLoader = entry.hasGps + ? GeocodingService.getAddress(entry.latLng!, settings.appliedLocale).then((addresses) { + if (addresses.isNotEmpty) { + final address = addresses.first; + return { + 'addressLine': address.addressLine, + 'adminArea': address.adminArea, + 'countryCode': address.countryCode, + 'countryName': address.countryName, + 'featureName': address.featureName, + 'locality': address.locality, + 'postalCode': address.postalCode, + 'subAdminArea': address.subAdminArea, + 'subLocality': address.subLocality, + 'subThoroughfare': address.subThoroughfare, + 'thoroughfare': address.thoroughfare, + }; + } + return {}; + }) + : Future.value({}); setState(() {}); } @@ -77,10 +100,7 @@ class _MetadataTabState extends State<MetadataTab> { if (data.isNotEmpty) Padding( padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: InfoRowGroup( - info: data, - maxValueLength: Constants.infoGroupMaxValueLength, - ), + child: InfoRowGroup(info: data), ) ], ); @@ -152,6 +172,10 @@ class _MetadataTabState extends State<MetadataTab> { ); }, ), + FutureBuilder<Map>( + future: _addressLoader, + builder: (context, snapshot) => builderFromSnapshot(context, snapshot, 'Address'), + ), ], ); } diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 3446ca8b4..978ab17c3 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'dart:ui'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/catalog.dart'; import 'package:aves/model/entry/extensions/location.dart'; @@ -21,6 +20,7 @@ import 'package:aves/widgets/viewer/info/info_page.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 900943741..9f98fe485 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -1,8 +1,7 @@ +import 'dart:async'; import 'dart:math'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; @@ -10,12 +9,11 @@ import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/filters/filters.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/settings/enums/accessibility_timeout.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves_utils/aves_utils.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; @@ -27,15 +25,18 @@ import 'package:aves/widgets/viewer/entry_vertical_pager.dart'; import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/overlay/bottom.dart'; +import 'package:aves/widgets/viewer/overlay/locked.dart'; import 'package:aves/widgets/viewer/overlay/panorama.dart'; import 'package:aves/widgets/viewer/overlay/slideshow_buttons.dart'; import 'package:aves/widgets/viewer/overlay/top.dart'; import 'package:aves/widgets/viewer/overlay/video/video.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves_video/aves_video.dart'; import 'package:aves/widgets/viewer/visual/conductor.dart'; import 'package:aves/widgets/viewer/visual/controller_mixin.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:aves_utils/aves_utils.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart'; @@ -69,6 +70,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr final AChangeNotifier _verticalScrollNotifier = AChangeNotifier(); bool _overlayInitialized = false; final ValueNotifier<bool> _overlayVisible = ValueNotifier(true); + final ValueNotifier<bool> _viewLocked = ValueNotifier(false); final ValueNotifier<bool> _overlayExpandedNotifier = ValueNotifier(false); late AnimationController _overlayAnimationController; late Animation<double> _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity; @@ -77,6 +79,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr late VideoActionDelegate _videoActionDelegate; final ValueNotifier<HeroInfo?> _heroInfoNotifier = ValueNotifier(null); bool _isEntryTracked = true; + Timer? _overlayHidingTimer; @override bool get isViewingImage => _currentVerticalPage.value == imagePage; @@ -146,6 +149,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr )); _overlayVisible.value = settings.showOverlayOnOpening && !viewerController.autopilot; _overlayVisible.addListener(_onOverlayVisibleChanged); + _viewLocked.addListener(_onViewLockedChanged); _videoActionDelegate = VideoActionDelegate( collection: collection, ); @@ -169,9 +173,11 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr _videoActionDelegate.dispose(); _overlayAnimationController.dispose(); _overlayVisible.dispose(); + _viewLocked.dispose(); _overlayExpandedNotifier.dispose(); _verticalPager.dispose(); _heroInfoNotifier.dispose(); + _stopOverlayHidingTimer(); WidgetsBinding.instance.removeObserver(this); _unregisterWidget(widget); super.dispose(); @@ -249,16 +255,33 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr stream: device.supportPictureInPicture ? _floating.pipStatus$ : Stream.value(PiPStatus.disabled), builder: (context, snapshot) { var pipEnabled = snapshot.data == PiPStatus.enabled; - return Stack( - children: [ - viewer, - if (!pipEnabled) ...[ - ..._buildOverlays(availableSize).map(_decorateOverlay), - const TopGestureAreaProtector(), - const SideGestureAreaProtector(), - const BottomGestureAreaProtector(), - ], - ], + return ValueListenableBuilder<bool>( + valueListenable: _viewLocked, + builder: (context, locked, child) { + return Stack( + children: [ + child!, + if (!pipEnabled) ...[ + if (locked) ...[ + const Positioned.fill( + child: AbsorbPointer(), + ), + Positioned.fill( + child: GestureDetector( + onTap: () => _overlayVisible.value = !_overlayVisible.value, + ), + ), + _buildViewerLockedBottomOverlay(), + ] else + ..._buildOverlays(availableSize).map(_decorateOverlay), + const TopGestureAreaProtector(), + const SideGestureAreaProtector(), + const BottomGestureAreaProtector(), + ], + ], + ); + }, + child: viewer, ); }, ); @@ -300,6 +323,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr } } + Widget _buildViewerLockedBottomOverlay() { + return TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: ViewerLockedOverlay( + animationController: _overlayAnimationController, + ), + ); + } + Widget _buildSlideshowBottomOverlay(Size availableSize) { return SizedBox.fromSize( size: availableSize, @@ -490,6 +524,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr } } else if (notification is ToggleOverlayNotification) { _overlayVisible.value = notification.visible ?? !_overlayVisible.value; + } else if (notification is LockViewNotification) { + _viewLocked.value = notification.locked; } else if (notification is VideoActionNotification) { _onVideoAction( context: context, @@ -544,16 +580,16 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr _verticalScrollNotifier.notify(); } - void _goToCollection(CollectionFilter filter) { + Future<void> _goToCollection(CollectionFilter filter) async { final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main; if (!isMainMode) return; final baseCollection = collection; if (baseCollection == null) return; - _onLeave(); + unawaited(_onLeave()); final uri = entryNotifier.value?.uri; - Navigator.maybeOf(context)?.pushAndRemoveUntil( + unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( @@ -563,7 +599,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr ), ), (route) => false, - ); + )); } Future<void> _goToVerticalPage(int page) async { @@ -704,8 +740,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr void _popVisual() { if (Navigator.canPop(context)) { - void pop() { - _onLeave(); + Future<void> pop() async { + unawaited(_onLeave()); Navigator.maybeOf(context)?.pop(); } @@ -747,13 +783,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr } Future<void> _onLeave() async { + // get the theme first, as the context is likely + // to be unmounted after the other async steps + final theme = Theme.of(context); + await ScreenBrightness().resetScreenBrightness(); if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { await windowService.keepScreenOn(false); } await mediaSessionService.release(); await AvesApp.showSystemUI(); - AvesApp.setSystemUIStyle(context); + AvesApp.setSystemUIStyle(theme); if (!settings.useTvLayout) { await windowService.requestOrientation(); } @@ -804,8 +844,12 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr Future<void> _onOverlayVisibleChanged({bool animate = true}) async { if (!mounted) return; if (_overlayVisible.value) { - await AvesApp.showSystemUI(); - AvesApp.setSystemUIStyle(context); + if (_viewLocked.value) { + await _startOverlayHidingTimer(); + } else { + await AvesApp.showSystemUI(); + AvesApp.setSystemUIStyle(Theme.of(context)); + } if (animate) { await _overlayAnimationController.forward(); } else { @@ -830,4 +874,24 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr }); } } + + Future<void> _onViewLockedChanged() async { + if (_viewLocked.value) { + await AvesApp.hideSystemUI(); + await _startOverlayHidingTimer(); + } else { + await AvesApp.showSystemUI(); + AvesApp.setSystemUIStyle(Theme.of(context)); + _stopOverlayHidingTimer(); + _overlayVisible.value = true; + } + } + + Future<void> _startOverlayHidingTimer() async { + _stopOverlayHidingTimer(); + final duration = await settings.timeToTakeAction.getSnackBarDuration(true); + _overlayHidingTimer = Timer(duration, () => _overlayVisible.value = false); + } + + void _stopOverlayHidingTimer() => _overlayHidingTimer?.cancel(); } diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index b1da03c60..db32c6994 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -1,6 +1,6 @@ import 'package:aves/app_mode.dart'; import 'package:aves/image_providers/app_icon_image_provider.dart'; -import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/apps.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/favourites.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; @@ -14,17 +14,19 @@ import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/type.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/format.dart'; -import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/rate_button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/tag_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -273,9 +275,12 @@ class _BasicInfoState extends State<_BasicInfo> { AvesEntry get entry => widget.entry; - int get megaPixels => entry.megaPixels; + int get megaPixels => (entry.width * entry.height / 1000000).round(); - bool get showMegaPixels => entry.isPhoto && megaPixels > 0; + // guess whether this is a photo, according to file type + bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(entry.mimeType) || entry.isRaw; + + bool get showMegaPixels => isPhoto && megaPixels > 0; String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}'; @@ -291,7 +296,7 @@ class _BasicInfoState extends State<_BasicInfo> { }); final isViewerMode = context.read<ValueNotifier<AppMode>>().value == AppMode.view; if (isViewerMode && settings.isInstalledAppAccessAllowed) { - _appNameLoader = androidFileUtils.initAppNames(); + _appNameLoader = appInventory.initAppNames(); } } } @@ -349,7 +354,7 @@ class _BasicInfoState extends State<_BasicInfo> { InfoValueSpanBuilder _ownerHandler(String? ownerPackage) { if (ownerPackage == null) return (context, key, value) => []; - final appName = androidFileUtils.getCurrentAppName(ownerPackage) ?? ownerPackage; + final appName = appInventory.getCurrentAppName(ownerPackage) ?? ownerPackage; return (context, key, value) => [ WidgetSpan( alignment: PlaceholderAlignment.middle, diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index 8f73ced5e..6d4981c57 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -45,8 +45,9 @@ class InfoRowGroup extends StatefulWidget { final int maxValueLength; final Map<String, InfoValueSpanBuilder> spanBuilders; - static const keyValuePadding = 16; - static const fontSize = 13.0; + static const int defaultMaxValueLength = 140; + static const double keyValuePadding = 16; + static const double fontSize = 13; static const valueStyle = TextStyle(fontSize: fontSize); static final _keyStyle = valueStyle.copyWith(height: 2.0); @@ -55,7 +56,7 @@ class InfoRowGroup extends StatefulWidget { const InfoRowGroup({ super.key, required this.info, - this.maxValueLength = 0, + this.maxValueLength = defaultMaxValueLength, Map<String, InfoValueSpanBuilder>? spanBuilders, }) : spanBuilders = spanBuilders ?? const {}; @@ -75,7 +76,7 @@ class InfoRowGroup extends StatefulWidget { final linkColor = Theme.of(context).colorScheme.primary; final style = InfoRowGroup.valueStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); - return [TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', style: style, recognizer: recognizer)]; + return [TextSpan(text: '${Unicode.FSI}$value${Unicode.PDI}', style: style, recognizer: recognizer)]; }; } } @@ -97,7 +98,7 @@ class _InfoRowGroupState extends State<InfoRowGroup> { // compute the size of keys and space in order to align values final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: key, style: _keyStyle), textScaleFactor)))); + final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: _buildTextValue(key), style: _keyStyle), textScaleFactor)))); final lastKey = keyValues.keys.last; return LayoutBuilder( @@ -115,15 +116,11 @@ class _InfoRowGroupState extends State<InfoRowGroup> { final spanBuilder = spanBuilders[key] ?? _buildTextValueSpans; final thisSpaceSize = max(0.0, (baseValueX - keySizes[key]!)) + InfoRowGroup.keyValuePadding; - // each text span embeds and pops a Bidi isolate, - // so that layout of the spans follows the directionality of the locale - // (e.g. keys on the right for RTL locale, whatever the key intrinsic directionality) - // and each span respects the directionality of its inner text only return [ - TextSpan(text: '${Constants.fsi}$key${Constants.pdi}', style: _keyStyle), + TextSpan(text: _buildTextValue(key), style: _keyStyle), WidgetSpan( child: SizedBox( - width: thisSpaceSize, + width: thisSpaceSize / textScaleFactor, // as of Flutter v3.0.0, the underline decoration from the following `TextSpan` // is applied to the `WidgetSpan` too, so we add a dummy `Text` as a workaround child: const Text(''), @@ -161,8 +158,14 @@ class _InfoRowGroupState extends State<InfoRowGroup> { recognizer = TapGestureRecognizer()..onTap = () => setState(() => _expandedKeys.add(key)); } - return [TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', recognizer: recognizer)]; + return [TextSpan(text: _buildTextValue(value), recognizer: recognizer)]; } + + // each text span embeds and pops a Bidi isolate, + // so that layout of the spans follows the directionality of the locale + // (e.g. keys on the right for RTL locale, whatever the key intrinsic directionality) + // and each span respects the directionality of its inner text only + String _buildTextValue(String value) => '${Unicode.FSI}$value${Unicode.PDI}'; } typedef InfoValueSpanBuilder = List<InlineSpan> Function(BuildContext context, String key, String value); diff --git a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart index ff96ebdcd..287a712fb 100644 --- a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart @@ -62,10 +62,10 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin { final uri = fields['uri']!; if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) { // open with another app - unawaited(androidAppService.open(uri, mimeType, forceChooser: true).then((success) { + unawaited(appService.open(uri, mimeType, forceChooser: true).then((success) { if (!success) { // fallback to sharing, so that the file can be saved somewhere - androidAppService.shareSingle(uri, mimeType).then((success) { + appService.shareSingle(uri, mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); }); } diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 689152407..69f2b910e 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,5 +1,4 @@ import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/selection.dart'; @@ -7,13 +6,16 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar/sliver_app_bar_title.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.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_dir.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -49,12 +51,14 @@ class InfoAppBar extends StatelessWidget { return SliverAppBar( leading: useTvLayout ? null - : IconButton( - // key is expected by test driver - key: const Key('back-button'), - icon: const Icon(AIcons.goUp), - onPressed: onBackPressed, - tooltip: context.l10n.viewerInfoBackToViewerTooltip, + : FontSizeIconTheme( + child: IconButton( + // key is expected by test driver + key: const Key('back-button'), + icon: const Icon(AIcons.goUp), + onPressed: onBackPressed, + tooltip: context.l10n.viewerInfoBackToViewerTooltip, + ), ), automaticallyImplyLeading: false, title: SliverAppBarTitleWrapper( @@ -72,27 +76,25 @@ class InfoAppBar extends StatelessWidget { tooltip: MaterialLocalizations.of(context).searchFieldLabel, ), if (entry.canEdit) - MenuIconTheme( - child: PopupMenuButton<EntryAction>( - itemBuilder: (context) => [ - ...commonActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), - if (formatSpecificActions.isNotEmpty) ...[ - const PopupMenuDivider(), - ...formatSpecificActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), - ], - if (!kReleaseMode) ...[ - const PopupMenuDivider(), - _toMenuItem(context, EntryAction.debug, enabled: true), - ] + PopupMenuButton<EntryAction>( + itemBuilder: (context) => [ + ...commonActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), + if (formatSpecificActions.isNotEmpty) ...[ + const PopupMenuDivider(), + ...formatSpecificActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), ], - onSelected: (action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - actionDelegate.onActionSelected(context, entry, collection, action); - }, - ), + if (!kReleaseMode) ...[ + const PopupMenuDivider(), + _toMenuItem(context, EntryAction.debug, enabled: true), + ] + ], + onSelected: (action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + actionDelegate.onActionSelected(context, entry, collection, action); + }, ), - ], + ].map((v) => FontSizeIconTheme(child: v)).toList(), floating: true, ); } diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 3820af71c..78b65ce3e 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -1,9 +1,8 @@ import 'dart:async'; -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/actions/events.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; +import 'package:aves/model/events.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -22,6 +21,7 @@ import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 158e44a6e..6e5abc3f4 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -103,6 +103,8 @@ class _LocationSectionState extends State<LocationSection> { final address = entry.addressDetails!; final country = address.countryName; if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}')); + final state = address.stateName; + if (state != null && state.isNotEmpty) filters.add(LocationFilter(LocationLevel.state, '$state${LocationFilter.locationSeparator}${address.stateCode}')); final place = address.place; if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place)); } diff --git a/lib/widgets/viewer/info/metadata/geotiff.dart b/lib/widgets/viewer/info/metadata/geotiff.dart index 3fdb84fca..fb647b987 100644 --- a/lib/widgets/viewer/info/metadata/geotiff.dart +++ b/lib/widgets/viewer/info/metadata/geotiff.dart @@ -1,4 +1,4 @@ -import 'package:aves/ref/geotiff.dart'; +import 'package:aves/ref/metadata/geotiff.dart'; class GeoTiffDirectory { // TODO TLAD [geotiff] avoid string-based match diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index c353eb785..b999e44ce 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -4,7 +4,6 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart'; import 'package:aves/theme/colors.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/common.dart'; @@ -117,7 +116,6 @@ class MetadataDirTileBody extends StatelessWidget { padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup( info: tags, - maxValueLength: Constants.infoGroupMaxValueLength, spanBuilders: linkHandlers, ), ), diff --git a/lib/widgets/viewer/info/metadata/xmp_card.dart b/lib/widgets/viewer/info/metadata/xmp_card.dart index 840a46983..a7185fb1b 100644 --- a/lib/widgets/viewer/info/metadata/xmp_card.dart +++ b/lib/widgets/viewer/info/metadata/xmp_card.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/multi_cross_fader.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; @@ -127,7 +126,6 @@ class _XmpCardState extends State<XmpCard> { padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup( info: Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, widget.formatValue(prop)))), - maxValueLength: Constants.infoGroupMaxValueLength, spanBuilders: widget.spanBuilders?.call(_isIndexed ? index + 1 : null), ), ), diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index 7264a8c40..bb99034b6 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -1,8 +1,9 @@ import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/theme/colors.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_card.dart'; @@ -38,52 +39,54 @@ class XmpNamespace extends Equatable { factory XmpNamespace.create(Map<String, String> schemaRegistryPrefixes, String nsPrefix, Map<String, String> rawProps) { final nsUri = schemaRegistryPrefixes[nsPrefix] ?? ''; switch (nsUri) { - case Namespaces.creatorAtom: + case XmpNamespaces.creatorAtom: return XmpCreatorAtom(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.crs: + case XmpNamespaces.crs: return XmpCrsNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.darktable: + case XmpNamespaces.darktable: return XmpDarktableNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.dwc: + case XmpNamespaces.dwc: return XmpDwcNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.exif: + case XmpNamespaces.exif: return XmpExifNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.gAudio: + case XmpNamespaces.gAudio: return XmpGAudioNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.gCamera: + case XmpNamespaces.gCamera: return XmpGCameraNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.gContainer: + case XmpNamespaces.gContainer: return XmpGContainer(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.gDepth: + case XmpNamespaces.gDepth: return XmpGDepthNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.gDevice: + case XmpNamespaces.gDevice: return XmpGDeviceNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.gImage: + case XmpNamespaces.gImage: return XmpGImageNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.iptc4xmpCore: + case XmpNamespaces.iptc4xmpCore: return XmpIptcCoreNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.iptc4xmpExt: + case XmpNamespaces.iptc4xmpExt: return XmpIptc4xmpExtNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.mwgrs: + case XmpNamespaces.mwgrs: return XmpMgwRegionsNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.mp: + case XmpNamespaces.mp: return XmpMPNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.photoshop: + case XmpNamespaces.photoshop: return XmpPhotoshopNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.plus: + case XmpNamespaces.plus: return XmpPlusNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.tiff: + case XmpNamespaces.tiff: return XmpTiffNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.xmp: + case XmpNamespaces.xmp: return XmpBasicNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); - case Namespaces.xmpMM: + case XmpNamespaces.xmpMM: return XmpMMNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); + case XmpNamespaces.xperiaCamera: + return XmpXperiaCameraNamespace(schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); default: return XmpNamespace(nsUri: nsUri, schemaRegistryPrefixes: schemaRegistryPrefixes, rawProps: rawProps); } } - String get displayTitle => Namespaces.nsTitles[nsUri] ?? (nsPrefix.isEmpty ? nsUri : '${nsPrefix.substring(0, nsPrefix.length - 1)} ($nsUri)'); + String get displayTitle => XmpNamespaceView.nsTitles[nsUri] ?? (nsPrefix.isEmpty ? nsUri : '${nsPrefix.substring(0, nsPrefix.length - 1)} ($nsUri)'); List<Widget> buildNamespaceSection(BuildContext context) { final props = rawProps.entries @@ -101,7 +104,6 @@ class XmpNamespace extends Equatable { if (props.isNotEmpty) InfoRowGroup( info: Map.fromEntries(props.map((v) => MapEntry(v.displayKey, formatValue(v)))), - maxValueLength: Constants.infoGroupMaxValueLength, spanBuilders: linkifyValues(props), ), ...cards.where((v) => !v.isEmpty).map((card) { diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart b/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart index cae67a6d6..15e987d00 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart @@ -1,8 +1,8 @@ -import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; class XmpCrsNamespace extends XmpNamespace { - XmpCrsNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.crs); + XmpCrsNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.crs); @override late final List<XmpCardData> cards = [ diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart b/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart index d0778f570..bbbf6771e 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart @@ -1,10 +1,10 @@ -import 'package:aves/ref/exif.dart'; -import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/ref/metadata/exif.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/exif.md class XmpExifNamespace extends XmpNamespace { - XmpExifNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.exif); + XmpExifNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.exif); @override String formatValue(XmpProp prop) { diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 5b0834a13..fca1e0d8c 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -1,4 +1,4 @@ -import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/embedded/notifications.dart'; @@ -57,7 +57,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace { } class XmpGAudioNamespace extends XmpGoogleNamespace { - XmpGAudioNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gAudio); + XmpGAudioNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.gAudio); @override List<Tuple2<String, String>> get dataProps => [ @@ -66,7 +66,7 @@ class XmpGAudioNamespace extends XmpGoogleNamespace { } class XmpGCameraNamespace extends XmpGoogleNamespace { - XmpGCameraNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gCamera); + XmpGCameraNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.gCamera); @override List<Tuple2<String, String>> get dataProps => [ @@ -75,7 +75,7 @@ class XmpGCameraNamespace extends XmpGoogleNamespace { } class XmpGContainer extends XmpNamespace { - XmpGContainer({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gContainer); + XmpGContainer({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.gContainer); @override late final List<XmpCardData> cards = [ @@ -84,7 +84,7 @@ class XmpGContainer extends XmpNamespace { } class XmpGDepthNamespace extends XmpGoogleNamespace { - XmpGDepthNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gDepth); + XmpGDepthNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.gDepth); @override List<Tuple2<String, String>> get dataProps => [ @@ -98,10 +98,10 @@ class XmpGDeviceNamespace extends XmpNamespace { late final String _containerNsPrefix; late final String _itemNsPrefix; - XmpGDeviceNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gDevice) { - _cameraNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, Namespaces.gDeviceCamera); - _containerNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, Namespaces.gDeviceContainer); - _itemNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, Namespaces.gDeviceItem); + XmpGDeviceNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.gDevice) { + _cameraNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, XmpNamespaces.gDeviceCamera); + _containerNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, XmpNamespaces.gDeviceContainer); + _itemNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, XmpNamespaces.gDeviceItem); final mimePattern = RegExp(nsPrefix + r'Container/' + _containerNsPrefix + r'Directory\[(\d+)\]/' + _itemNsPrefix + r'Mime'); final originalProps = rawProps.entries.toList(); @@ -153,7 +153,7 @@ class XmpGDeviceNamespace extends XmpNamespace { } class XmpGImageNamespace extends XmpGoogleNamespace { - XmpGImageNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.gImage); + XmpGImageNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.gImage); @override List<Tuple2<String, String>> get dataProps => [ diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart b/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart index a47ffccd3..168d92ddb 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart @@ -1,8 +1,8 @@ -import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; class XmpCreatorAtom extends XmpNamespace { - XmpCreatorAtom({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.creatorAtom); + XmpCreatorAtom({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.creatorAtom); @override late final List<XmpCardData> cards = [ @@ -11,7 +11,7 @@ class XmpCreatorAtom extends XmpNamespace { } class XmpDarktableNamespace extends XmpNamespace { - XmpDarktableNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.darktable); + XmpDarktableNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.darktable); @override late final List<XmpCardData> cards = [ @@ -20,7 +20,7 @@ class XmpDarktableNamespace extends XmpNamespace { } class XmpDwcNamespace extends XmpNamespace { - XmpDwcNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.dwc); + XmpDwcNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.dwc); @override late final List<XmpCardData> cards = [ @@ -37,7 +37,7 @@ class XmpDwcNamespace extends XmpNamespace { } class XmpIptcCoreNamespace extends XmpNamespace { - XmpIptcCoreNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.iptc4xmpCore); + XmpIptcCoreNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.iptc4xmpCore); @override late final List<XmpCardData> cards = [ @@ -46,7 +46,7 @@ class XmpIptcCoreNamespace extends XmpNamespace { } class XmpIptc4xmpExtNamespace extends XmpNamespace { - XmpIptc4xmpExtNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.iptc4xmpExt); + XmpIptc4xmpExtNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.iptc4xmpExt); @override late final List<XmpCardData> cards = [ @@ -55,7 +55,7 @@ class XmpIptc4xmpExtNamespace extends XmpNamespace { } class XmpMPNamespace extends XmpNamespace { - XmpMPNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.mp); + XmpMPNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.mp); @override late final List<XmpCardData> cards = [ @@ -65,7 +65,7 @@ class XmpMPNamespace extends XmpNamespace { // cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15) class XmpMgwRegionsNamespace extends XmpNamespace { - XmpMgwRegionsNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.mwgrs); + XmpMgwRegionsNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.mwgrs); @override late final List<XmpCardData> cards = [ @@ -75,7 +75,7 @@ class XmpMgwRegionsNamespace extends XmpNamespace { } class XmpPlusNamespace extends XmpNamespace { - XmpPlusNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.plus); + XmpPlusNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.plus); @override late final List<XmpCardData> cards = [ @@ -86,7 +86,7 @@ class XmpPlusNamespace extends XmpNamespace { } class XmpMMNamespace extends XmpNamespace { - XmpMMNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.xmpMM); + XmpMMNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.xmpMM); @override late final List<XmpCardData> cards = [ @@ -102,3 +102,12 @@ class XmpMMNamespace extends XmpNamespace { ), ]; } + +class XmpXperiaCameraNamespace extends XmpNamespace { + XmpXperiaCameraNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.xperiaCamera); + + @override + late final List<XmpCardData> cards = [ + XmpCardData(RegExp(nsPrefix + r'Face\[(\d+)\]/(.*)')), + ]; +} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart b/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart index da01fd4f3..5449a896f 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart @@ -1,9 +1,9 @@ -import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md class XmpPhotoshopNamespace extends XmpNamespace { - XmpPhotoshopNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.photoshop); + XmpPhotoshopNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.photoshop); @override late final List<XmpCardData> cards = [ diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart b/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart index 5ff5ed4eb..b67df3ac6 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart @@ -1,10 +1,10 @@ -import 'package:aves/ref/exif.dart'; -import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/ref/metadata/exif.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/tiff.md class XmpTiffNamespace extends XmpNamespace { - XmpTiffNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.tiff); + XmpTiffNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.tiff); @override String formatValue(XmpProp prop) { diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index 78e874750..6bdf34d26 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart @@ -1,12 +1,12 @@ import 'package:aves/ref/mime_types.dart'; -import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/embedded/notifications.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; class XmpBasicNamespace extends XmpNamespace { - XmpBasicNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: Namespaces.xmp); + XmpBasicNamespace({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.xmp); @override late final List<XmpCardData> cards = [ @@ -19,9 +19,9 @@ class XmpBasicNamespace extends XmpNamespace { linkText: (context) => context.l10n.viewerInfoOpenLinkText, onTap: (context) => OpenEmbeddedDataNotification.xmp( props: [ - const [Namespaces.xmp, 'Thumbnails'], + const [XmpNamespaces.xmp, 'Thumbnails'], index, - const [Namespaces.xmpGImg, 'image'], + const [XmpNamespaces.xmpGImg, 'image'], ], mimeType: MimeTypes.jpeg, ).dispatch(context), diff --git a/lib/widgets/viewer/overlay/details/date.dart b/lib/widgets/viewer/overlay/details/date.dart index 67e24fadd..62abba441 100644 --- a/lib/widgets/viewer/overlay/details/date.dart +++ b/lib/widgets/viewer/overlay/details/date.dart @@ -2,7 +2,8 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; @@ -26,7 +27,7 @@ class OverlayDateRow extends StatelessWidget { final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat); final date = entry.bestDate; - final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown; + final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; final resolutionText = entry.isSvg ? entry.aspectRatioText : entry.isSized @@ -37,8 +38,8 @@ class OverlayDateRow extends StatelessWidget { children: [ DecoratedIcon(AIcons.date, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), const SizedBox(width: ViewerDetailOverlayContent.iconPadding), - Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), - Expanded(flex: 2, child: Text(resolutionText, strutStyle: Constants.overflowStrutStyle)), + Expanded(flex: 3, child: Text(dateText, strutStyle: AStyles.overflowStrut)), + Expanded(flex: 2, child: Text(resolutionText, strutStyle: AStyles.overflowStrut)), ], ); } diff --git a/lib/widgets/viewer/overlay/details/description.dart b/lib/widgets/viewer/overlay/details/description.dart index fec7ab219..171ecde5e 100644 --- a/lib/widgets/viewer/overlay/details/description.dart +++ b/lib/widgets/viewer/overlay/details/description.dart @@ -1,5 +1,5 @@ import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; @@ -31,7 +31,7 @@ class OverlayDescriptionRow extends StatelessWidget { TextSpan(text: description), ], ), - strutStyle: Constants.overflowStrutStyle, + strutStyle: AStyles.overflowStrut, ); } } diff --git a/lib/widgets/viewer/overlay/details/details.dart b/lib/widgets/viewer/overlay/details/details.dart index 3b10442c1..c7b4bba56 100644 --- a/lib/widgets/viewer/overlay/details/details.dart +++ b/lib/widgets/viewer/overlay/details/details.dart @@ -5,7 +5,7 @@ import 'package:aves/model/metadata/overlay.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/constants.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/details/date.dart'; import 'package:aves/widgets/viewer/overlay/details/description.dart'; @@ -135,7 +135,7 @@ class ViewerDetailOverlayContent extends StatelessWidget { static const double iconPadding = 8.0; static const double iconSize = 16.0; - static List<Shadow>? shadows(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? Constants.embossShadows : null; + static List<Shadow>? shadows(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? AStyles.embossShadows : null; const ViewerDetailOverlayContent({ super.key, diff --git a/lib/widgets/viewer/overlay/details/location.dart b/lib/widgets/viewer/overlay/details/location.dart index ad3ebd4af..b646168e6 100644 --- a/lib/widgets/viewer/overlay/details/location.dart +++ b/lib/widgets/viewer/overlay/details/location.dart @@ -3,7 +3,8 @@ import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; import 'package:decorated_icon/decorated_icon.dart'; @@ -19,22 +20,21 @@ class OverlayLocationRow extends AnimatedWidget { @override Widget build(BuildContext context) { - late final String location; + String? location; if (entry.hasAddress) { location = entry.shortAddress; - } else { + } + if (location == null || location.isEmpty) { final latLng = entry.latLng; if (latLng != null) { location = settings.coordinateFormat.format(context.l10n, latLng); - } else { - location = ''; } } return Row( children: [ DecoratedIcon(AIcons.location, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), const SizedBox(width: ViewerDetailOverlayContent.iconPadding), - Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)), + Expanded(child: Text(location ?? AText.valueNotAvailable, strutStyle: AStyles.overflowStrut)), ], ); } diff --git a/lib/widgets/viewer/overlay/details/position_title.dart b/lib/widgets/viewer/overlay/details/position_title.dart index d13640e95..167594b8b 100644 --- a/lib/widgets/viewer/overlay/details/position_title.dart +++ b/lib/widgets/viewer/overlay/details/position_title.dart @@ -1,7 +1,9 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/multipage.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class OverlayPositionTitleRow extends StatelessWidget { @@ -26,9 +28,9 @@ class OverlayPositionTitleRow extends StatelessWidget { [ if (collectionPosition != null) collectionPosition, if (pagePosition != null) pagePosition, - if (title != null) '${Constants.fsi}$title${Constants.pdi}', - ].join(Constants.separator), - strutStyle: Constants.overflowStrutStyle); + if (title != null) '${Unicode.FSI}$title${Unicode.PDI}', + ].join(AText.separator), + strutStyle: AStyles.overflowStrut); if (multiPageController == null) return toText(); diff --git a/lib/widgets/viewer/overlay/details/rating_tags.dart b/lib/widgets/viewer/overlay/details/rating_tags.dart index 9a837f478..8cda1194d 100644 --- a/lib/widgets/viewer/overlay/details/rating_tags.dart +++ b/lib/widgets/viewer/overlay/details/rating_tags.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; import 'package:collection/collection.dart'; @@ -31,6 +32,7 @@ class OverlayRatingTagsRow extends AnimatedWidget { break; } + final textScaleFactor = MediaQuery.textScaleFactorOf(context); final tags = entry.tags.toList()..sort(compareAsciiUpperCaseNatural); final hasTags = tags.isNotEmpty; @@ -39,23 +41,23 @@ class OverlayRatingTagsRow extends AnimatedWidget { children: [ TextSpan(text: ratingString), if (hasTags) ...[ - if (ratingString.isNotEmpty) const TextSpan(text: Constants.separator), + if (ratingString.isNotEmpty) const TextSpan(text: AText.separator), WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( padding: const EdgeInsetsDirectional.only(end: ViewerDetailOverlayContent.iconPadding), child: DecoratedIcon( AIcons.tag, - size: ViewerDetailOverlayContent.iconSize, + size: ViewerDetailOverlayContent.iconSize / textScaleFactor, shadows: ViewerDetailOverlayContent.shadows(context), ), ), ), - TextSpan(text: tags.join(Constants.separator)), + TextSpan(text: tags.join(AText.separator)), ] ], ), - strutStyle: Constants.overflowStrutStyle, + strutStyle: AStyles.overflowStrut, ); } } diff --git a/lib/widgets/viewer/overlay/details/shooting.dart b/lib/widgets/viewer/overlay/details/shooting.dart index 0d8ea39b9..567e652be 100644 --- a/lib/widgets/viewer/overlay/details/shooting.dart +++ b/lib/widgets/viewer/overlay/details/shooting.dart @@ -1,6 +1,7 @@ import 'package:aves/model/metadata/overlay.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; import 'package:decorated_icon/decorated_icon.dart'; @@ -20,22 +21,22 @@ class OverlayShootingRow extends StatelessWidget { final locale = context.l10n.localeName; final aperture = details.aperture; - final apertureText = aperture != null ? 'ƒ/${NumberFormat('0.0', locale).format(aperture)}' : Constants.overlayUnknown; + final apertureText = aperture != null ? 'ƒ/${NumberFormat('0.0', locale).format(aperture)}' : AText.valueNotAvailable; final focalLength = details.focalLength; - final focalLengthText = focalLength != null ? context.l10n.focalLength(NumberFormat('0.#', locale).format(focalLength)) : Constants.overlayUnknown; + final focalLengthText = focalLength != null ? context.l10n.focalLength(NumberFormat('0.#', locale).format(focalLength)) : AText.valueNotAvailable; final iso = details.iso; - final isoText = iso != null ? 'ISO$iso' : Constants.overlayUnknown; + final isoText = iso != null ? 'ISO$iso' : AText.valueNotAvailable; return Row( children: [ DecoratedIcon(AIcons.shooting, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), const SizedBox(width: ViewerDetailOverlayContent.iconPadding), - Expanded(child: Text(apertureText, strutStyle: Constants.overflowStrutStyle)), - Expanded(child: Text(details.exposureTime ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), - Expanded(child: Text(focalLengthText, strutStyle: Constants.overflowStrutStyle)), - Expanded(child: Text(isoText, strutStyle: Constants.overflowStrutStyle)), + Expanded(child: Text(apertureText, strutStyle: AStyles.overflowStrut)), + Expanded(child: Text(details.exposureTime ?? AText.valueNotAvailable, strutStyle: AStyles.overflowStrut)), + Expanded(child: Text(focalLengthText, strutStyle: AStyles.overflowStrut)), + Expanded(child: Text(isoText, strutStyle: AStyles.overflowStrut)), ], ); } diff --git a/lib/widgets/viewer/overlay/locked.dart b/lib/widgets/viewer/overlay/locked.dart new file mode 100644 index 000000000..57ec2484e --- /dev/null +++ b/lib/widgets/viewer/overlay/locked.dart @@ -0,0 +1,90 @@ +import 'dart:math'; + +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; +import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; +import 'package:aves/widgets/viewer/controls/notifications.dart'; +import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ViewerLockedOverlay extends StatefulWidget { + final AnimationController animationController; + final EdgeInsets? viewInsets, viewPadding; + + const ViewerLockedOverlay({ + super.key, + required this.animationController, + this.viewInsets, + this.viewPadding, + }); + + @override + State<StatefulWidget> createState() => _ViewerLockedOverlayState(); +} + +class _ViewerLockedOverlayState extends State<ViewerLockedOverlay> { + late Animation<double> _buttonScale; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant ViewerLockedOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(ViewerLockedOverlay widget) { + _buttonScale = CurvedAnimation( + parent: widget.animationController, + // a little bounce at the top + curve: Curves.easeOutBack, + ); + } + + void _unregisterWidget(ViewerLockedOverlay widget) { + // nothing + } + + @override + Widget build(BuildContext context) { + return Selector<MediaQueryData, double>( + selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom), + builder: (context, mqPaddingBottom, child) { + final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); + return Container( + alignment: AlignmentDirectional.bottomEnd, + padding: EdgeInsets.only(bottom: mqPaddingBottom) + const EdgeInsets.all(ViewerButtonRowContent.padding), + child: SafeArea( + top: false, + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, + ), + child: OverlayButton( + scale: _buttonScale, + child: IconButton( + icon: const Icon(AIcons.viewerUnlock), + onPressed: () => const LockViewNotification(locked: false).dispatch(context), + tooltip: context.l10n.viewerActionUnlock, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/widgets/viewer/overlay/selection_button.dart b/lib/widgets/viewer/overlay/selection_button.dart index 874492854..5a3c4ea53 100644 --- a/lib/widgets/viewer/overlay/selection_button.dart +++ b/lib/widgets/viewer/overlay/selection_button.dart @@ -2,7 +2,7 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/common/basic/text/animated_diff.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; @@ -50,7 +50,7 @@ class SelectionButton extends StatelessWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 8), - child: Text(Constants.separator), + child: Text(AText.separator), ), Selector<Selection<AvesEntry>, bool>( selector: (context, selection) => selection.isSelected({mainEntry}), diff --git a/lib/widgets/viewer/overlay/slideshow_buttons.dart b/lib/widgets/viewer/overlay/slideshow_buttons.dart index cbfee2a3b..bb42659a2 100644 --- a/lib/widgets/viewer/overlay/slideshow_buttons.dart +++ b/lib/widgets/viewer/overlay/slideshow_buttons.dart @@ -1,11 +1,12 @@ -import 'package:aves/model/actions/slideshow_actions.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/viewer/controls/intents.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart'; import 'package:aves/widgets/viewer/slideshow_page.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/widgets/viewer/overlay/video/controls.dart b/lib/widgets/viewer/overlay/video/controls.dart index d2522896f..e3f41e9ce 100644 --- a/lib/widgets/viewer/overlay/video/controls.dart +++ b/lib/widgets/viewer/overlay/video/controls.dart @@ -1,9 +1,9 @@ -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry/entry.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/togglers/play.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/overlay/video/progress_bar.dart b/lib/widgets/viewer/overlay/video/progress_bar.dart index aac65fe25..62a21c974 100644 --- a/lib/widgets/viewer/overlay/video/progress_bar.dart +++ b/lib/widgets/viewer/overlay/video/progress_bar.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/styles.dart'; import 'package:aves/theme/themes.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves_video/aves_video.dart'; @@ -42,7 +42,7 @@ class _VideoProgressBarState extends State<VideoProgressBar> { final blurred = settings.enableBlurEffect; final brightness = Theme.of(context).brightness; final textStyle = TextStyle( - shadows: brightness == Brightness.dark ? Constants.embossShadows : null, + shadows: brightness == Brightness.dark ? AStyles.embossShadows : null, ); return SizeTransition( sizeFactor: widget.scale, @@ -73,59 +73,64 @@ class _VideoProgressBarState extends State<VideoProgressBar> { border: AvesBorder.border(context), borderRadius: const BorderRadius.all(Radius.circular(radius)), ), - child: Column( - key: _progressBarKey, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - StreamBuilder<int>( - stream: positionStream, - builder: (context, snapshot) { - // do not use stream snapshot because it is obsolete when switching between videos - final position = controller?.currentPosition.floor() ?? 0; - return Text( - formatFriendlyDuration(Duration(milliseconds: position)), - style: textStyle, - ); - }), - const Spacer(), - Text( - formatFriendlyDuration(Duration(milliseconds: controller?.duration ?? 0)), - style: textStyle, - ), - ], - ), - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: Directionality( - // force directionality for `LinearProgressIndicator` - textDirection: TextDirection.ltr, - child: StreamBuilder<int>( - stream: positionStream, - builder: (context, snapshot) { - // do not use stream snapshot because it is obsolete when switching between videos - var progress = controller?.progress ?? 0.0; - if (!progress.isFinite) progress = 0.0; - return LinearProgressIndicator( - value: progress, - backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2), - ); - }), + child: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: 1, + ), + child: Column( + key: _progressBarKey, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + StreamBuilder<int>( + stream: positionStream, + builder: (context, snapshot) { + // do not use stream snapshot because it is obsolete when switching between videos + final position = controller?.currentPosition.floor() ?? 0; + return Text( + formatFriendlyDuration(Duration(milliseconds: position)), + style: textStyle, + ); + }), + const Spacer(), + Text( + formatFriendlyDuration(Duration(milliseconds: controller?.duration ?? 0)), + style: textStyle, + ), + ], ), - ), - Row( - children: [ - _buildSpeedIndicator(), - _buildMuteIndicator(), - Text( - // fake text below to match the height of the text above and center the whole thing - '', - style: textStyle, + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Directionality( + // force directionality for `LinearProgressIndicator` + textDirection: TextDirection.ltr, + child: StreamBuilder<int>( + stream: positionStream, + builder: (context, snapshot) { + // do not use stream snapshot because it is obsolete when switching between videos + var progress = controller?.progress ?? 0.0; + if (!progress.isFinite) progress = 0.0; + return LinearProgressIndicator( + value: progress, + backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2), + ); + }), ), - ], - ), - ], + ), + Row( + children: [ + _buildSpeedIndicator(), + _buildMuteIndicator(), + Text( + // fake text below to match the height of the text above and center the whole thing + '', + style: textStyle, + ), + ], + ), + ], + ), ), ), ), diff --git a/lib/widgets/viewer/overlay/video/video.dart b/lib/widgets/viewer/overlay/video/video.dart index 409ea7bd2..5272fa414 100644 --- a/lib/widgets/viewer/overlay/video/video.dart +++ b/lib/widgets/viewer/overlay/video/video.dart @@ -1,10 +1,11 @@ import 'dart:async'; -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry/entry.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/viewer/overlay/video/controls.dart'; import 'package:aves/widgets/viewer/overlay/video/progress_bar.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index 52a27e5db..fbb31c322 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; @@ -9,6 +8,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/move_button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/rate_button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/share_button.dart'; @@ -16,6 +16,7 @@ import 'package:aves/widgets/common/action_controls/quick_choosers/tag_button.da import 'package:aves/widgets/common/action_controls/togglers/favourite.dart'; import 'package:aves/widgets/common/action_controls/togglers/mute.dart'; import 'package:aves/widgets/common/action_controls/togglers/play.dart'; +import 'package:aves/widgets/common/basic/font_size_icon_theme.dart'; import 'package:aves/widgets/common/basic/popup/container.dart'; import 'package:aves/widgets/common/basic/popup/expansion_panel.dart'; import 'package:aves/widgets/common/basic/popup/menu_button.dart'; @@ -26,6 +27,7 @@ import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/viewer/action/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -240,7 +242,7 @@ class ViewerButtonRowContent extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: padding / 2), child: OverlayButton( scale: scale, - child: MenuIconTheme( + child: FontSizeIconTheme( child: AvesPopupMenuButton<EntryAction>( key: const Key('entry-menu-button'), itemBuilder: (context) { @@ -285,6 +287,7 @@ class ViewerButtonRowContent extends StatelessWidget { onCanceled: () { _popupExpandedNotifier.value = null; }, + iconSize: IconTheme.of(context).size, onMenuOpened: () { // if the menu is opened while overlay is hiding, // the popup menu button is disposed and menu items are ineffective, diff --git a/lib/widgets/viewer/overlay/wallpaper_buttons.dart b/lib/widgets/viewer/overlay/wallpaper_buttons.dart index 3d949f033..2ff880700 100644 --- a/lib/widgets/viewer/overlay/wallpaper_buttons.dart +++ b/lib/widgets/viewer/overlay/wallpaper_buttons.dart @@ -5,7 +5,7 @@ import 'dart:ui' as ui; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/model/entry/extensions/props.dart'; -import 'package:aves/model/wallpaper_target.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves/services/wallpaper_service.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 00a97cb45..71067e16d 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -166,7 +166,7 @@ class _PanoramaPageState extends State<PanoramaPage> { Future<void> _onLeave() async { await AvesApp.showSystemUI(); - AvesApp.setSystemUIStyle(context); + AvesApp.setSystemUIStyle(Theme.of(context)); } // system UI @@ -183,7 +183,7 @@ class _PanoramaPageState extends State<PanoramaPage> { Future<void> _onOverlayVisibleChanged() async { if (_overlayVisible.value) { await AvesApp.showSystemUI(); - AvesApp.setSystemUIStyle(context); + AvesApp.setSystemUIStyle(Theme.of(context)); } else { await AvesApp.hideSystemUI(); } diff --git a/lib/widgets/viewer/screen_saver_page.dart b/lib/widgets/viewer/screen_saver_page.dart index cc370fc39..90bc7fc40 100644 --- a/lib/widgets/viewer/screen_saver_page.dart +++ b/lib/widgets/viewer/screen_saver_page.dart @@ -1,5 +1,4 @@ import 'package:aves/model/filters/mime.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'; @@ -11,6 +10,7 @@ import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:aves/widgets/viewer/providers.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/slideshow_page.dart b/lib/widgets/viewer/slideshow_page.dart index 48bcd0e2e..d4139d669 100644 --- a/lib/widgets/viewer/slideshow_page.dart +++ b/lib/widgets/viewer/slideshow_page.dart @@ -1,9 +1,7 @@ import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/slideshow_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/mime.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'; @@ -17,6 +15,7 @@ import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:aves/widgets/viewer/providers.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/video/conductor.dart b/lib/widgets/viewer/video/conductor.dart index 7178949a4..dfe886e18 100644 --- a/lib/widgets/viewer/video/conductor.dart +++ b/lib/widgets/viewer/video/conductor.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.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'; -import 'package:aves_video/aves_video.dart'; import 'package:aves/widgets/viewer/video/db_playback_state_handler.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; class VideoConductor { diff --git a/lib/widgets/viewer/visual/controller_mixin.dart b/lib/widgets/viewer/visual/controller_mixin.dart index 6f39dd025..52235df48 100644 --- a/lib/widgets/viewer/visual/controller_mixin.dart +++ b/lib/widgets/viewer/visual/controller_mixin.dart @@ -2,12 +2,12 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.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/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -133,6 +133,8 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> { } Future<void> _initMultiPageController(AvesEntry entry) async { + if (!mounted) return; + final multiPageController = context.read<MultiPageConductor>().getOrCreateController(entry); setState(() {}); diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index fff2d55d1..30bf70574 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; @@ -9,6 +8,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/media_session_service.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/viewer/controls/controller.dart'; @@ -25,6 +25,7 @@ import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart'; import 'package:aves/widgets/viewer/visual/video/swipe_action.dart'; import 'package:aves/widgets/viewer/visual/video/video_view.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 1924c3fad..40704ae49 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -5,11 +5,11 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/model/entry/extensions/props.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/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index 5d583d31d..5a198cc43 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -5,12 +5,12 @@ import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/images.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'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; diff --git a/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart b/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart index 3e9f24cba..e6b78b844 100644 --- a/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart +++ b/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart @@ -1,3 +1,4 @@ +import 'package:aves/ref/unicode.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/line.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart'; @@ -39,8 +40,6 @@ class AssParser { // e.g. m 937.5 472.67 b 937.5 472.67 937.25 501.25 960 501.5 960 501.5 937.5 500.33 937.5 529.83 static final pathPattern = RegExp(r'([mnlbspc])([.\s\d]+)'); - static const noBreakSpace = '\u00A0'; - // Parse text with ASS style overrides // cf https://aegi.vmoe.info/docs/3.0/ASS_Tags/ // e.g. `And I'm like, "We can't {\i1}not{\i0} see it."` @@ -389,7 +388,7 @@ class AssParser { ); } - static String _replaceChars(String text) => text.replaceAll(r'\h', noBreakSpace).replaceAll(r'\N', '\n').trim(); + static String _replaceChars(String text) => text.replaceAll(r'\h', UniChars.noBreakSpace).replaceAll(r'\N', '\n').trim(); static int? _parseAlpha(String param) { final match = alphaPattern.firstMatch(param); diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index 7d398f5d3..2c85c7d34 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -1,8 +1,6 @@ -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.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/durations.dart'; @@ -21,6 +19,7 @@ import 'package:aves/widgets/viewer/providers.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves/widgets/viewer/visual/controller_mixin.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -262,7 +261,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin Future<void> _onOverlayVisibleChanged({bool animate = true}) async { if (_overlayVisible.value) { await AvesApp.showSystemUI(); - AvesApp.setSystemUIStyle(context); + AvesApp.setSystemUIStyle(Theme.of(context)); if (animate) { await _overlayAnimationController.forward(); } else { diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 2959174b0..a06e1f902 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -3,7 +3,7 @@ import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/widgets/about/policy_page.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/markdown_container.dart'; @@ -174,7 +174,7 @@ class _WelcomePageState extends State<WelcomePage> { value: settings.isInstalledAppAccessAllowed, onChanged: (v) => setState(() => settings.isInstalledAppAccessAllowed = v), title: Text(l10n.settingsAllowInstalledAppAccess), - subtitle: Text([l10n.welcomeOptional, l10n.settingsAllowInstalledAppAccessSubtitle].join(Constants.separator)), + subtitle: Text([l10n.welcomeOptional, l10n.settingsAllowInstalledAppAccessSubtitle].join(AText.separator)), contentPadding: contentPadding, ), if (canEnableErrorReporting) diff --git a/plugins/aves_magnifier/lib/src/controller/controller.dart b/plugins/aves_magnifier/lib/src/controller/controller.dart index 3e1695375..89858643f 100644 --- a/plugins/aves_magnifier/lib/src/controller/controller.dart +++ b/plugins/aves_magnifier/lib/src/controller/controller.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:aves_magnifier/src/controller/state.dart'; import 'package:aves_magnifier/src/scale/scale_boundaries.dart'; @@ -20,17 +19,13 @@ class AvesMagnifierController { AvesMagnifierController({ MagnifierState? initialState, }) : super() { - initial = initialState ?? - const MagnifierState( - position: Offset.zero, - scale: null, - source: ChangeSource.internal, - ); + const source = ChangeSource.internal; + initial = initialState ?? const MagnifierState(position: Offset.zero, scale: null, source: source); previousState = initial; _currentState = initial; _setState(initial); - const _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: ChangeSource.internal); + const _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: source); previousScaleState = _initialScaleState; _currentScaleState = _initialScaleState; _setScaleState(_initialScaleState); diff --git a/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart b/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart index c2de57bbb..21b373790 100644 --- a/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart +++ b/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart @@ -80,15 +80,20 @@ mixin AvesMagnifierControllerDelegate on State<MagnifierCore> { Offset get position => controller.position; + double? recalcScale() { + final scaleState = controller.scaleState.state; + final newScale = controller.getScaleForScaleState(scaleState); + markNeedsScaleRecalc = false; + setScale(newScale, ChangeSource.internal); + return newScale; + } + double? get scale { final scaleState = controller.scaleState.state; final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut); final scaleExistsOnController = controller.scale != null; if (needsRecalc || !scaleExistsOnController) { - final newScale = controller.getScaleForScaleState(scaleState); - markNeedsScaleRecalc = false; - setScale(newScale, ChangeSource.internal); - return newScale; + return recalcScale(); } return controller.scale; } diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart index 58edae587..de9829584 100644 --- a/plugins/aves_magnifier/lib/src/core/core.dart +++ b/plugins/aves_magnifier/lib/src/core/core.dart @@ -72,6 +72,9 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM ..addStatusListener(onAnimationStatus); _positionAnimationController = AnimationController(vsync: this)..addListener(handlePositionAnimate); _registerWidget(widget); + // force delegate scale computing on initialization + // so that it does not happen lazily at the beginning of a scale animation + recalcScale(); } @override diff --git a/plugins/aves_magnifier/pubspec.lock b/plugins/aves_magnifier/pubspec.lock index 26dfee840..03b594497 100644 --- a/plugins/aves_magnifier/pubspec.lock +++ b/plugins/aves_magnifier/pubspec.lock @@ -108,5 +108,5 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=1.16.0" diff --git a/plugins/aves_magnifier/pubspec.yaml b/plugins/aves_magnifier/pubspec.yaml index 083facfdd..14f197fcd 100644 --- a/plugins/aves_magnifier/pubspec.yaml +++ b/plugins/aves_magnifier/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_map/pubspec.lock b/plugins/aves_map/pubspec.lock index f0229daf1..ffeeaa6f0 100644 --- a/plugins/aves_map/pubspec.lock +++ b/plugins/aves_map/pubspec.lock @@ -222,10 +222,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" string_scanner: dependency: transitive description: @@ -283,5 +283,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_map/pubspec.yaml b/plugins/aves_map/pubspec.yaml index 15b1316cd..8f325c421 100644 --- a/plugins/aves_map/pubspec.yaml +++ b/plugins/aves_map/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_model/lib/aves_model.dart b/plugins/aves_model/lib/aves_model.dart index d892a954c..f03a09657 100644 --- a/plugins/aves_model/lib/aves_model.dart +++ b/plugins/aves_model/lib/aves_model.dart @@ -1,5 +1,24 @@ library aves_model; +export 'src/actions/chip.dart'; +export 'src/actions/chip_set.dart'; +export 'src/actions/entry.dart'; +export 'src/actions/entry_set.dart'; +export 'src/actions/map.dart'; +export 'src/actions/map_cluster.dart'; +export 'src/actions/move_type.dart'; +export 'src/actions/settings.dart'; +export 'src/actions/share.dart'; +export 'src/actions/slideshow.dart'; export 'src/entry/base.dart'; +export 'src/metadata/enums.dart'; +export 'src/metadata/fields.dart'; +export 'src/settings/enums.dart'; +export 'src/source/album.dart'; +export 'src/source/enums.dart'; +export 'src/source/vault.dart'; +export 'src/storage/relative_dir.dart'; +export 'src/storage/volume.dart'; export 'src/video/keys.dart'; export 'src/video/stream_types.dart'; +export 'src/wallpaper_target.dart'; diff --git a/plugins/aves_model/lib/src/actions/chip.dart b/plugins/aves_model/lib/src/actions/chip.dart new file mode 100644 index 000000000..e4cd6789b --- /dev/null +++ b/plugins/aves_model/lib/src/actions/chip.dart @@ -0,0 +1,9 @@ +enum ChipAction { + goToAlbumPage, + goToCountryPage, + goToPlacePage, + goToTagPage, + reverse, + hide, + lockVault, +} diff --git a/plugins/aves_model/lib/src/actions/chip_set.dart b/plugins/aves_model/lib/src/actions/chip_set.dart new file mode 100644 index 000000000..6493ba2c5 --- /dev/null +++ b/plugins/aves_model/lib/src/actions/chip_set.dart @@ -0,0 +1,67 @@ +enum ChipSetAction { + // general + configureView, + select, + selectAll, + selectNone, + // browsing + search, + toggleTitleSearch, + createAlbum, + createVault, + // browsing or selecting + map, + slideshow, + stats, + // selecting (single/multiple filters) + delete, + hide, + pin, + unpin, + lockVault, + showCountryStates, + // selecting (single filter) + rename, + setCover, + configureVault, +} + +class ChipSetActions { + static const general = [ + ChipSetAction.configureView, + ChipSetAction.select, + ChipSetAction.selectAll, + ChipSetAction.selectNone, + ]; + + // `null` items are converted to dividers + static const browsing = [ + ChipSetAction.search, + ChipSetAction.toggleTitleSearch, + null, + ChipSetAction.map, + ChipSetAction.slideshow, + ChipSetAction.stats, + null, + ChipSetAction.createAlbum, + ChipSetAction.createVault, + ]; + + // `null` items are converted to dividers + static const selection = [ + ChipSetAction.setCover, + ChipSetAction.pin, + ChipSetAction.unpin, + ChipSetAction.delete, + ChipSetAction.rename, + ChipSetAction.showCountryStates, + ChipSetAction.hide, + null, + ChipSetAction.map, + ChipSetAction.slideshow, + ChipSetAction.stats, + null, + ChipSetAction.configureVault, + ChipSetAction.lockVault, + ]; +} diff --git a/plugins/aves_model/lib/src/actions/entry.dart b/plugins/aves_model/lib/src/actions/entry.dart new file mode 100644 index 000000000..e0a6b68c9 --- /dev/null +++ b/plugins/aves_model/lib/src/actions/entry.dart @@ -0,0 +1,137 @@ +enum EntryAction { + info, + addShortcut, + copyToClipboard, + delete, + restore, + convert, + print, + rename, + copy, + move, + share, + toggleFavourite, + // raster + rotateCCW, + rotateCW, + flip, + // vector + viewSource, + // video + lockViewer, + videoCaptureFrame, + videoSelectStreams, + videoSetSpeed, + videoToggleMute, + videoSettings, + videoTogglePlay, + videoReplay10, + videoSkip10, + // external + edit, + open, + openVideo, + openMap, + setAs, + // platform + rotateScreen, + // metadata + editDate, + editLocation, + editTitleDescription, + editRating, + editTags, + removeMetadata, + exportMetadata, + // metadata / GeoTIFF + showGeoTiffOnMap, + // metadata / motion photo + convertMotionPhotoToStillImage, + viewMotionPhotoVideo, + // debug + debug, +} + +class EntryActions { + static const topLevel = [ + EntryAction.info, + EntryAction.share, + EntryAction.edit, + EntryAction.rename, + EntryAction.delete, + EntryAction.copy, + EntryAction.move, + EntryAction.toggleFavourite, + EntryAction.rotateScreen, + EntryAction.viewSource, + ]; + + static const export = [ + ...exportInternal, + ...exportExternal, + ]; + + static const exportInternal = [ + EntryAction.convert, + EntryAction.addShortcut, + EntryAction.copyToClipboard, + EntryAction.print, + ]; + + static const exportExternal = [ + EntryAction.open, + EntryAction.openMap, + EntryAction.setAs, + ]; + + static const pageActions = { + EntryAction.videoCaptureFrame, + EntryAction.videoSelectStreams, + EntryAction.videoSetSpeed, + EntryAction.videoToggleMute, + EntryAction.videoSettings, + EntryAction.videoTogglePlay, + EntryAction.videoReplay10, + EntryAction.videoSkip10, + EntryAction.rotateCCW, + EntryAction.rotateCW, + EntryAction.flip, + }; + + static const trashed = [ + EntryAction.delete, + EntryAction.restore, + EntryAction.debug, + ]; + + static const video = [ + EntryAction.videoCaptureFrame, + EntryAction.videoToggleMute, + EntryAction.videoSetSpeed, + EntryAction.videoSelectStreams, + EntryAction.videoSettings, + EntryAction.lockViewer, + ]; + + static const videoPlayback = [ + EntryAction.videoReplay10, + EntryAction.videoTogglePlay, + EntryAction.videoSkip10, + ]; + + static const commonMetadataActions = [ + EntryAction.editDate, + EntryAction.editLocation, + EntryAction.editTitleDescription, + EntryAction.editRating, + EntryAction.editTags, + EntryAction.removeMetadata, + EntryAction.exportMetadata, + ]; + + static const formatSpecificMetadataActions = [ + EntryAction.showGeoTiffOnMap, + EntryAction.convertMotionPhotoToStillImage, + EntryAction.viewMotionPhotoVideo, + ]; +} diff --git a/plugins/aves_model/lib/src/actions/entry_set.dart b/plugins/aves_model/lib/src/actions/entry_set.dart new file mode 100644 index 000000000..a13697756 --- /dev/null +++ b/plugins/aves_model/lib/src/actions/entry_set.dart @@ -0,0 +1,125 @@ +enum EntrySetAction { + // general + configureView, + select, + selectAll, + selectNone, + // browsing + searchCollection, + toggleTitleSearch, + addShortcut, + emptyBin, + // browsing or selecting + map, + slideshow, + stats, + rescan, + // selecting + share, + delete, + restore, + copy, + move, + rename, + convert, + toggleFavourite, + rotateCCW, + rotateCW, + flip, + editDate, + editLocation, + editTitleDescription, + editRating, + editTags, + removeMetadata, +} + +class EntrySetActions { + static const general = [ + EntrySetAction.configureView, + EntrySetAction.select, + EntrySetAction.selectAll, + EntrySetAction.selectNone, + ]; + + // `null` items are converted to dividers + static const pageBrowsing = [ + EntrySetAction.searchCollection, + EntrySetAction.toggleTitleSearch, + EntrySetAction.addShortcut, + null, + EntrySetAction.map, + EntrySetAction.slideshow, + EntrySetAction.stats, + null, + EntrySetAction.rescan, + EntrySetAction.emptyBin, + ]; + + // exclude bin related actions + static const collectionEditorBrowsing = [ + EntrySetAction.searchCollection, + EntrySetAction.toggleTitleSearch, + EntrySetAction.addShortcut, + EntrySetAction.map, + EntrySetAction.slideshow, + EntrySetAction.stats, + EntrySetAction.rescan, + ]; + + // `null` items are converted to dividers + static const pageSelection = [ + EntrySetAction.share, + EntrySetAction.delete, + EntrySetAction.restore, + EntrySetAction.copy, + EntrySetAction.move, + EntrySetAction.rename, + EntrySetAction.convert, + EntrySetAction.toggleFavourite, + null, + EntrySetAction.map, + EntrySetAction.slideshow, + EntrySetAction.stats, + null, + EntrySetAction.rescan, + // editing actions are in their subsection + ]; + + // exclude bin related actions + static const collectionEditorSelectionRegular = [ + EntrySetAction.share, + EntrySetAction.delete, + EntrySetAction.copy, + EntrySetAction.move, + EntrySetAction.rename, + EntrySetAction.convert, + EntrySetAction.toggleFavourite, + EntrySetAction.map, + EntrySetAction.slideshow, + EntrySetAction.stats, + EntrySetAction.rescan, + // editing actions are in their subsection + ]; + + static const collectionEditorSelectionEdit = [ + EntrySetAction.rotateCCW, + EntrySetAction.rotateCW, + EntrySetAction.flip, + EntrySetAction.editDate, + EntrySetAction.editLocation, + EntrySetAction.editTitleDescription, + EntrySetAction.editRating, + EntrySetAction.editTags, + EntrySetAction.removeMetadata, + ]; + + static const edit = [ + EntrySetAction.editDate, + EntrySetAction.editLocation, + EntrySetAction.editTitleDescription, + EntrySetAction.editRating, + EntrySetAction.editTags, + EntrySetAction.removeMetadata, + ]; +} diff --git a/plugins/aves_model/lib/src/actions/map.dart b/plugins/aves_model/lib/src/actions/map.dart new file mode 100644 index 000000000..f9be8b8d0 --- /dev/null +++ b/plugins/aves_model/lib/src/actions/map.dart @@ -0,0 +1,5 @@ +enum MapAction { + selectStyle, + zoomIn, + zoomOut, +} diff --git a/plugins/aves_model/lib/src/actions/map_cluster.dart b/plugins/aves_model/lib/src/actions/map_cluster.dart new file mode 100644 index 000000000..d02293435 --- /dev/null +++ b/plugins/aves_model/lib/src/actions/map_cluster.dart @@ -0,0 +1,4 @@ +enum MapClusterAction { + editLocation, + removeLocation, +} diff --git a/plugins/aves_model/lib/src/actions/move_type.dart b/plugins/aves_model/lib/src/actions/move_type.dart new file mode 100644 index 000000000..7c362b0cc --- /dev/null +++ b/plugins/aves_model/lib/src/actions/move_type.dart @@ -0,0 +1,7 @@ +enum MoveType { + copy, + move, + export, + toBin, + fromBin, +} diff --git a/lib/model/actions/settings_actions.dart b/plugins/aves_model/lib/src/actions/settings.dart similarity index 100% rename from lib/model/actions/settings_actions.dart rename to plugins/aves_model/lib/src/actions/settings.dart diff --git a/plugins/aves_model/lib/src/actions/share.dart b/plugins/aves_model/lib/src/actions/share.dart new file mode 100644 index 000000000..3be6fb681 --- /dev/null +++ b/plugins/aves_model/lib/src/actions/share.dart @@ -0,0 +1,4 @@ +enum ShareAction { + imageOnly, + videoOnly, +} diff --git a/plugins/aves_model/lib/src/actions/slideshow.dart b/plugins/aves_model/lib/src/actions/slideshow.dart new file mode 100644 index 000000000..bb0facbd6 --- /dev/null +++ b/plugins/aves_model/lib/src/actions/slideshow.dart @@ -0,0 +1,5 @@ +enum SlideshowAction { + resume, + showInCollection, + settings, +} diff --git a/lib/model/metadata/enums/enums.dart b/plugins/aves_model/lib/src/metadata/enums.dart similarity index 73% rename from lib/model/metadata/enums/enums.dart rename to plugins/aves_model/lib/src/metadata/enums.dart index ad1641fae..7e933d5b2 100644 --- a/lib/model/metadata/enums/enums.dart +++ b/plugins/aves_model/lib/src/metadata/enums.dart @@ -46,3 +46,25 @@ enum MetadataType { // XMP: https://en.wikipedia.org/wiki/Extensible_Metadata_Platform xmp, } + +class MetadataTypes { + static const main = { + MetadataType.exif, + MetadataType.xmp, + }; + + static const common = { + MetadataType.exif, + MetadataType.xmp, + MetadataType.comment, + MetadataType.iccProfile, + MetadataType.iptc, + MetadataType.photoshopIrb, + }; + + static const jpeg = { + MetadataType.jfif, + MetadataType.jpegAdobe, + MetadataType.jpegDucky, + }; +} diff --git a/plugins/aves_model/lib/src/metadata/fields.dart b/plugins/aves_model/lib/src/metadata/fields.dart new file mode 100644 index 000000000..29c7ca15b --- /dev/null +++ b/plugins/aves_model/lib/src/metadata/fields.dart @@ -0,0 +1,80 @@ +enum MetadataField { + exifDate, + exifDateOriginal, + exifDateDigitized, + exifGpsAltitude, + exifGpsAltitudeRef, + exifGpsAreaInformation, + exifGpsDatestamp, + exifGpsDestBearing, + exifGpsDestBearingRef, + exifGpsDestDistance, + exifGpsDestDistanceRef, + exifGpsDestLatitude, + exifGpsDestLatitudeRef, + exifGpsDestLongitude, + exifGpsDestLongitudeRef, + exifGpsDifferential, + exifGpsDOP, + exifGpsHPositioningError, + exifGpsImgDirection, + exifGpsImgDirectionRef, + exifGpsLatitude, + exifGpsLatitudeRef, + exifGpsLongitude, + exifGpsLongitudeRef, + exifGpsMapDatum, + exifGpsMeasureMode, + exifGpsProcessingMethod, + exifGpsSatellites, + exifGpsSpeed, + exifGpsSpeedRef, + exifGpsStatus, + exifGpsTimestamp, + exifGpsTrack, + exifGpsTrackRef, + exifGpsVersionId, + exifImageDescription, + exifUserComment, + mp4GpsCoordinates, + mp4RotationDegrees, + mp4Xmp, + xmpXmpCreateDate, +} + +class MetadataFields { + static const Set<MetadataField> exifGpsFields = { + MetadataField.exifGpsAltitude, + MetadataField.exifGpsAltitudeRef, + MetadataField.exifGpsAreaInformation, + MetadataField.exifGpsDatestamp, + MetadataField.exifGpsDestBearing, + MetadataField.exifGpsDestBearingRef, + MetadataField.exifGpsDestDistance, + MetadataField.exifGpsDestDistanceRef, + MetadataField.exifGpsDestLatitude, + MetadataField.exifGpsDestLatitudeRef, + MetadataField.exifGpsDestLongitude, + MetadataField.exifGpsDestLongitudeRef, + MetadataField.exifGpsDifferential, + MetadataField.exifGpsDOP, + MetadataField.exifGpsHPositioningError, + MetadataField.exifGpsImgDirection, + MetadataField.exifGpsImgDirectionRef, + MetadataField.exifGpsLatitude, + MetadataField.exifGpsLatitudeRef, + MetadataField.exifGpsLongitude, + MetadataField.exifGpsLongitudeRef, + MetadataField.exifGpsMapDatum, + MetadataField.exifGpsMeasureMode, + MetadataField.exifGpsProcessingMethod, + MetadataField.exifGpsSatellites, + MetadataField.exifGpsSpeed, + MetadataField.exifGpsSpeedRef, + MetadataField.exifGpsStatus, + MetadataField.exifGpsTimestamp, + MetadataField.exifGpsTrack, + MetadataField.exifGpsTrackRef, + MetadataField.exifGpsVersionId, + }; +} diff --git a/lib/model/settings/enums/enums.dart b/plugins/aves_model/lib/src/settings/enums.dart similarity index 100% rename from lib/model/settings/enums/enums.dart rename to plugins/aves_model/lib/src/settings/enums.dart diff --git a/plugins/aves_model/lib/src/source/album.dart b/plugins/aves_model/lib/src/source/album.dart new file mode 100644 index 000000000..1520e8b3f --- /dev/null +++ b/plugins/aves_model/lib/src/source/album.dart @@ -0,0 +1,10 @@ +enum AlbumType { + regular, + vault, + app, + camera, + download, + screenRecordings, + screenshots, + videoCaptures, +} diff --git a/lib/model/source/enums/enums.dart b/plugins/aves_model/lib/src/source/enums.dart similarity index 100% rename from lib/model/source/enums/enums.dart rename to plugins/aves_model/lib/src/source/enums.dart diff --git a/plugins/aves_model/lib/src/source/vault.dart b/plugins/aves_model/lib/src/source/vault.dart new file mode 100644 index 000000000..b56094861 --- /dev/null +++ b/plugins/aves_model/lib/src/source/vault.dart @@ -0,0 +1 @@ +enum VaultLockType { system, pattern, pin, password } diff --git a/plugins/aves_model/lib/src/storage/relative_dir.dart b/plugins/aves_model/lib/src/storage/relative_dir.dart new file mode 100644 index 000000000..393178f25 --- /dev/null +++ b/plugins/aves_model/lib/src/storage/relative_dir.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +class VolumeRelativeDirectory extends Equatable { + final String volumePath, relativeDir; + + @override + List<Object?> get props => [volumePath, relativeDir]; + + String get dirPath => '$volumePath$relativeDir'; + + const VolumeRelativeDirectory({ + required this.volumePath, + required this.relativeDir, + }); + + static VolumeRelativeDirectory fromMap(Map map) { + return VolumeRelativeDirectory( + volumePath: map['volumePath'] ?? '', + relativeDir: map['relativeDir'] ?? '', + ); + } + + Map<String, dynamic> toMap() => { + 'volumePath': volumePath, + 'relativeDir': relativeDir, + }; +} diff --git a/plugins/aves_model/lib/src/storage/volume.dart b/plugins/aves_model/lib/src/storage/volume.dart new file mode 100644 index 000000000..ff6df3dd8 --- /dev/null +++ b/plugins/aves_model/lib/src/storage/volume.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +class StorageVolume extends Equatable { + final String? description; + final String path, state; + final bool isPrimary, isRemovable; + + @override + List<Object?> get props => [description, path, state, isPrimary, isRemovable]; + + const StorageVolume({ + required this.description, + required this.isPrimary, + required this.isRemovable, + required this.path, + required this.state, + }); + + factory StorageVolume.fromMap(Map map) { + final isPrimary = map['isPrimary'] ?? false; + return StorageVolume( + description: map['description'], + isPrimary: isPrimary, + isRemovable: map['isRemovable'] ?? false, + path: map['path'] ?? '', + state: map['state'] ?? '', + ); + } +} diff --git a/plugins/aves_model/lib/src/video/keys.dart b/plugins/aves_model/lib/src/video/keys.dart index 47a84bc84..9cb9e3fbd 100644 --- a/plugins/aves_model/lib/src/video/keys.dart +++ b/plugins/aves_model/lib/src/video/keys.dart @@ -3,6 +3,8 @@ // that write additional metadata to media files class Keys { static const androidCaptureFramerate = 'com.android.capture.fps'; + static const androidManufacturer = 'com.android.manufacturer'; + static const androidModel = 'com.android.model'; static const androidVersion = 'com.android.version'; static const bps = 'bps'; static const bitrate = 'bitrate'; @@ -31,6 +33,12 @@ class Keys { static const mediaFormat = 'format'; static const mediaType = 'media_type'; static const minorVersion = 'minor_version'; + static const quicktimeCreationDate = 'com.apple.quicktime.creationdate'; + static const quicktimeLocationAccuracyHorizontal = 'com.apple.quicktime.location.accuracy.horizontal'; + static const quicktimeLocationIso6709 = 'com.apple.quicktime.location.iso6709'; + static const quicktimeMake = 'com.apple.quicktime.make'; + static const quicktimeModel = 'com.apple.quicktime.model'; + static const quicktimeSoftware = 'com.apple.quicktime.software'; static const rotate = 'rotate'; static const sampleRate = 'sample_rate'; static const sarDen = 'sar_den'; @@ -50,4 +58,5 @@ class Keys { static const title = 'title'; static const track = 'track'; static const width = 'width'; + static const xiaomiSlowMoment = 'com.xiaomi.slow_moment'; } diff --git a/plugins/aves_model/lib/src/wallpaper_target.dart b/plugins/aves_model/lib/src/wallpaper_target.dart new file mode 100644 index 000000000..210a363c0 --- /dev/null +++ b/plugins/aves_model/lib/src/wallpaper_target.dart @@ -0,0 +1 @@ +enum WallpaperTarget { home, lock, homeLock } diff --git a/plugins/aves_model/pubspec.lock b/plugins/aves_model/pubspec.lock index 6e38c33fc..206fcaf52 100644 --- a/plugins/aves_model/pubspec.lock +++ b/plugins/aves_model/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" flutter: dependency: "direct main" description: flutter @@ -55,7 +63,7 @@ packages: source: hosted version: "0.2.0" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" @@ -76,4 +84,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" diff --git a/plugins/aves_model/pubspec.yaml b/plugins/aves_model/pubspec.yaml index 440c3befc..e07e9d4d0 100644 --- a/plugins/aves_model/pubspec.yaml +++ b/plugins/aves_model/pubspec.yaml @@ -3,11 +3,13 @@ version: 0.0.1 publish_to: none environment: - sdk: '>=2.19.4 <3.0.0' + sdk: '>=2.19.6 <3.0.0' dependencies: flutter: sdk: flutter + equatable: + meta: dev_dependencies: flutter_lints: diff --git a/plugins/aves_platform_meta/.gitignore b/plugins/aves_platform_meta/.gitignore index 28124a571..96486fd93 100644 --- a/plugins/aves_platform_meta/.gitignore +++ b/plugins/aves_platform_meta/.gitignore @@ -23,7 +23,7 @@ migrate_working_dir/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -#/pubspec.lock +/pubspec.lock **/doc/api/ .dart_tool/ .packages diff --git a/plugins/aves_platform_meta/android/build.gradle b/plugins/aves_platform_meta/android/build.gradle index 097b07644..a5f0fa1d4 100644 --- a/plugins/aves_platform_meta/android/build.gradle +++ b/plugins/aves_platform_meta/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -44,7 +44,3 @@ android { minSdkVersion 16 } } - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/plugins/aves_platform_meta/pubspec.lock b/plugins/aves_platform_meta/pubspec.lock index d4fa7deb8..10b974b12 100644 --- a/plugins/aves_platform_meta/pubspec.lock +++ b/plugins/aves_platform_meta/pubspec.lock @@ -84,4 +84,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" diff --git a/plugins/aves_platform_meta/pubspec.yaml b/plugins/aves_platform_meta/pubspec.yaml index a33a46065..67620cb1d 100644 --- a/plugins/aves_platform_meta/pubspec.yaml +++ b/plugins/aves_platform_meta/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report/pubspec.lock b/plugins/aves_report/pubspec.lock index 6e38c33fc..c656d7419 100644 --- a/plugins/aves_report/pubspec.lock +++ b/plugins/aves_report/pubspec.lock @@ -76,4 +76,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" diff --git a/plugins/aves_report/pubspec.yaml b/plugins/aves_report/pubspec.yaml index 25b1d5315..14de85cf7 100644 --- a/plugins/aves_report/pubspec.yaml +++ b/plugins/aves_report/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report_console/pubspec.lock b/plugins/aves_report_console/pubspec.lock index e92d2d71c..9be92d272 100644 --- a/plugins/aves_report_console/pubspec.lock +++ b/plugins/aves_report_console/pubspec.lock @@ -83,4 +83,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" diff --git a/plugins/aves_report_console/pubspec.yaml b/plugins/aves_report_console/pubspec.yaml index ee9350738..93fde45cf 100644 --- a/plugins/aves_report_console/pubspec.yaml +++ b/plugins/aves_report_console/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report_crashlytics/pubspec.lock b/plugins/aves_report_crashlytics/pubspec.lock index e3180cc2c..962c2e4e7 100644 --- a/plugins/aves_report_crashlytics/pubspec.lock +++ b/plugins/aves_report_crashlytics/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "330d7fcbb72624f5b6d374af8b059b0ef4ba96ba5b8987f874964a1287eb617d" + sha256: f175bc1414e4edf8c5b83372c98eeabecf8353f39c9da423c2cfdf1f1f508788 url: "https://pub.dev" source: hosted - version: "1.0.18" + version: "1.1.0" async: dependency: transitive description: @@ -68,42 +68,42 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "75f747cafd7cbd6c00b908e3a7aa59fc31593d46ba8165d9ee8a79e69464a394" + sha256: ed611fb8e67e43ecc7956f242cecca383d87cf71aace27287aa5dd4bdba4ac07 url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.9.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" + sha256: "0df0a064ab0cad7f8836291ca6f3272edd7b83ad5b3540478ee46a0849d8022b" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "0c1cf1f1022d2245ac117443bb95207952ca770281524d2908e323bc063fb8ff" + sha256: "347351a8f0518f3343d79a9a0690fa67ad232fc32e2ea270677791949eac792b" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "5410b6ab2009fc6c181ca82e16525fdcb324c5851933bfbb49246543b1005ebc" + sha256: "42cf6a137eaae7e485e6cc9794336e8e518c506b691aa6e19ff918206c535bec" url: "https://pub.dev" source: hosted - version: "3.0.17" + version: "3.1.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: bf3d3791c51a8448413b5ebd8388fa7db239a3e165db6bbd8ab58ef0e8f61906 + sha256: baa4c3d4af426d29800f0d80d165f31df4548985db151fd761346e07ed433d31 url: "https://pub.dev" source: hosted - version: "3.3.17" + version: "3.4.0" flutter: dependency: "direct main" description: flutter @@ -245,5 +245,5 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=1.20.0" diff --git a/plugins/aves_report_crashlytics/pubspec.yaml b/plugins/aves_report_crashlytics/pubspec.yaml index 75a38426c..445a12a4f 100644 --- a/plugins/aves_report_crashlytics/pubspec.yaml +++ b/plugins/aves_report_crashlytics/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_screen_state/.gitignore b/plugins/aves_screen_state/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/plugins/aves_screen_state/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_screen_state/.metadata b/plugins/aves_screen_state/.metadata new file mode 100644 index 000000000..20225b172 --- /dev/null +++ b/plugins/aves_screen_state/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f72efea43c3013323d1b95cff571f3c1caa37583 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + - platform: android + create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/plugins/aves_screen_state/analysis_options.yaml b/plugins/aves_screen_state/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_screen_state/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_screen_state/android/.gitignore b/plugins/aves_screen_state/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/plugins/aves_screen_state/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/plugins/aves_screen_state/android/build.gradle b/plugins/aves_screen_state/android/build.gradle new file mode 100644 index 000000000..62f47b1d4 --- /dev/null +++ b/plugins/aves_screen_state/android/build.gradle @@ -0,0 +1,46 @@ +group 'deckers.thibault.aves.aves_screen_state' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.20' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.4.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } +} diff --git a/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.jar b/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..41d9927a4 Binary files /dev/null and b/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.properties b/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..41dfb8790 --- /dev/null +++ b/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/plugins/aves_screen_state/android/gradlew b/plugins/aves_screen_state/android/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/plugins/aves_screen_state/android/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/plugins/aves_screen_state/android/gradlew.bat b/plugins/aves_screen_state/android/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/plugins/aves_screen_state/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugins/aves_screen_state/android/settings.gradle b/plugins/aves_screen_state/android/settings.gradle new file mode 100644 index 000000000..f3d6d44f3 --- /dev/null +++ b/plugins/aves_screen_state/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'aves_screen_state' diff --git a/plugins/aves_screen_state/android/src/main/AndroidManifest.xml b/plugins/aves_screen_state/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0c066880c --- /dev/null +++ b/plugins/aves_screen_state/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="deckers.thibault.aves.aves_screen_state"> +</manifest> diff --git a/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/AvesScreenStatePlugin.kt b/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/AvesScreenStatePlugin.kt new file mode 100644 index 000000000..04f330eca --- /dev/null +++ b/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/AvesScreenStatePlugin.kt @@ -0,0 +1,40 @@ +package deckers.thibault.aves.aves_screen_state + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink + +class AvesScreenStatePlugin : FlutterPlugin, EventChannel.StreamHandler { + private lateinit var eventChannel: EventChannel + private var context: Context? = null + private var screenReceiver: ScreenReceiver? = null + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "deckers.thibault/aves_screen_state/events") + context = flutterPluginBinding.applicationContext + eventChannel.setStreamHandler(this) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + eventChannel.setStreamHandler(null) + } + + override fun onListen(arguments: Any?, events: EventSink) { + screenReceiver = ScreenReceiver(events) + + val filter = IntentFilter().apply { + addAction(Intent.ACTION_SCREEN_ON) // Turn on + addAction(Intent.ACTION_SCREEN_OFF) // Turn off + addAction(Intent.ACTION_USER_PRESENT) // Unlock + } + context!!.registerReceiver(screenReceiver, filter) + } + + override fun onCancel(arguments: Any?) { + context!!.unregisterReceiver(screenReceiver) + } +} \ No newline at end of file diff --git a/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/ScreenReceiver.kt b/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/ScreenReceiver.kt new file mode 100644 index 000000000..a03fd7561 --- /dev/null +++ b/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/ScreenReceiver.kt @@ -0,0 +1,12 @@ +package deckers.thibault.aves.aves_screen_state + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import io.flutter.plugin.common.EventChannel.EventSink + +class ScreenReceiver(private val eventSink: EventSink) : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + eventSink.success(intent.action) + } +} \ No newline at end of file diff --git a/plugins/aves_screen_state/lib/aves_screen_state.dart b/plugins/aves_screen_state/lib/aves_screen_state.dart new file mode 100644 index 000000000..0e0b60880 --- /dev/null +++ b/plugins/aves_screen_state/lib/aves_screen_state.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; + +class AvesScreenState { + final EventChannel _eventChannel = const EventChannel('deckers.thibault/aves_screen_state/events'); + Stream<ScreenStateEvent>? _screenStateStream; + + Stream<ScreenStateEvent>? get screenStateStream { + if (Platform.isAndroid) { + _screenStateStream ??= _eventChannel.receiveBroadcastStream().map((event) => _parse(event as String)); + return _screenStateStream; + } + throw ScreenStateException('Screen State API is only available on Android.'); + } + + ScreenStateEvent _parse(String event) { + switch (event) { + case 'android.intent.action.SCREEN_OFF': + return ScreenStateEvent.off; + case 'android.intent.action.SCREEN_ON': + return ScreenStateEvent.on; + case 'android.intent.action.USER_PRESENT': + return ScreenStateEvent.unlocked; + default: + throw ArgumentError('$event was not recognized.'); + } + } +} + +enum ScreenStateEvent { unlocked, on, off } + +class ScreenStateException implements Exception { + final String _cause; + + ScreenStateException(this._cause); + + @override + String toString() => _cause; +} diff --git a/plugins/aves_screen_state/pubspec.yaml b/plugins/aves_screen_state/pubspec.yaml new file mode 100644 index 000000000..fda5d6290 --- /dev/null +++ b/plugins/aves_screen_state/pubspec.yaml @@ -0,0 +1,21 @@ +name: aves_screen_state +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=2.19.6 <3.0.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: + +dev_dependencies: + flutter_lints: + +flutter: + plugin: + platforms: + android: + package: deckers.thibault.aves.aves_screen_state + pluginClass: AvesScreenStatePlugin diff --git a/plugins/aves_services/pubspec.lock b/plugins/aves_services/pubspec.lock index acb670bd7..061d351c9 100644 --- a/plugins/aves_services/pubspec.lock +++ b/plugins/aves_services/pubspec.lock @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" string_scanner: dependency: transitive description: @@ -290,5 +290,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services/pubspec.yaml b/plugins/aves_services/pubspec.yaml index d2e60c871..cb818794f 100644 --- a/plugins/aves_services/pubspec.yaml +++ b/plugins/aves_services/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_services_google/lib/aves_services_platform.dart b/plugins/aves_services_google/lib/aves_services_platform.dart index 9b79f6101..ca5cadeac 100644 --- a/plugins/aves_services_google/lib/aves_services_platform.dart +++ b/plugins/aves_services_google/lib/aves_services_platform.dart @@ -20,23 +20,20 @@ class PlatformMobileServices extends MobileServices { _isAvailable = result == GooglePlayServicesAvailability.success; debugPrint('Device has Google Play Services=$_isAvailable'); - // as of google_maps_flutter v2.1.1, minSDK is 20 because of default PlatformView usage, - // but using hybrid composition would make it usable on API 19 too, - // cf https://github.com/flutter/flutter/issues/23728 - // as of google_maps_flutter v2.1.5, Flutter v3.0.1 makes the map hide overlay widgets on API <=22 final androidInfo = await DeviceInfoPlugin().androidInfo; _canRenderMaps = androidInfo.version.sdkInt >= 21; if (_canRenderMaps) { final mapsImplementation = GoogleMapsFlutterPlatform.instance; if (mapsImplementation is GoogleMapsFlutterAndroid) { - // as of google_maps_flutter_android v2.2.0, - // setting `useAndroidViewSurface` to true: + // as of flutter v3.7.10 / google_maps_flutter v2.2.5 / google_maps_flutter_android v2.4.10, + // setting `useAndroidViewSurface` to true (default): // + issue #241 exists but workaround is efficient - // + pan perf is OK when overlay is disabled - // - pan perf is bad when overlay is enabled + // - page stack and page animation perf is bad + // - overlay blur is disabled // setting `useAndroidViewSurface` to false: // - issue #241 exists and workaround is inefficient - // + pan perf is OK when overlay is disabled or enabled + // + page stack and page animation perf is OK + // + overlay blur is effective mapsImplementation.useAndroidViewSurface = false; } } diff --git a/plugins/aves_services_google/pubspec.lock b/plugins/aves_services_google/pubspec.lock index 7584f000a..048363840 100644 --- a/plugins/aves_services_google/pubspec.lock +++ b/plugins/aves_services_google/pubspec.lock @@ -66,10 +66,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" device_info_plus_platform_interface: dependency: transitive description: @@ -148,10 +148,26 @@ packages: dependency: "direct main" description: name: google_api_availability - sha256: "1642876fa87515fd5e4074458f22d6ba4518919c5abce16baaa2878c3c555678" + sha256: "3e9548cfd991d983d11425a2436d5bd957d048c279cc9e145ffe3f36fd847385" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" + google_api_availability_android: + dependency: transitive + description: + name: google_api_availability_android + sha256: eb309bc0b435731d18f306b598e176a9afcf642089a7d7c5cbb48e393afda345 + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + google_api_availability_platform_interface: + dependency: transitive + description: + name: google_api_availability_platform_interface + sha256: "65b7da62fe5b582bb3d508628ad827d36d890710ea274766a992a56fa5420da6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" google_maps_flutter: dependency: "direct main" description: @@ -164,10 +180,10 @@ packages: dependency: "direct main" description: name: google_maps_flutter_android - sha256: a8ee18649a67750cbd477a6867a1bf9c4154c5e9f69d722c8b53a627a6d58303 + sha256: ee3c1a63983b8ba17a9a7c1233c3542fbfbc0ebf540d3aa9b8fd5162681e0219 url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" google_maps_flutter_ios: dependency: transitive description: @@ -329,10 +345,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stream_transform: dependency: transitive description: @@ -393,10 +409,10 @@ packages: dependency: transitive description: name: win32 - sha256: "5cdbe09a75b5f4517adf213c68aaf53ffa162fadf54ba16f663f94f3d2664a56" + sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.3" wkt_parser: dependency: transitive description: @@ -406,5 +422,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_google/pubspec.yaml b/plugins/aves_services_google/pubspec.yaml index 259ac7a04..08a713a60 100644 --- a/plugins/aves_services_google/pubspec.yaml +++ b/plugins/aves_services_google/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_services_huawei/lib/aves_services_platform.dart b/plugins/aves_services_huawei/lib/aves_services_platform.dart index 467cb51a3..304642d8b 100644 --- a/plugins/aves_services_huawei/lib/aves_services_platform.dart +++ b/plugins/aves_services_huawei/lib/aves_services_platform.dart @@ -6,7 +6,7 @@ import 'package:aves_services/aves_services.dart'; import 'package:aves_services_platform/src/map.dart'; import 'package:flutter/widgets.dart'; import 'package:huawei_hmsavailability/huawei_hmsavailability.dart'; -import 'package:huawei_map/map.dart' as hmap; +import 'package:huawei_map/huawei_map.dart' as hmap; import 'package:latlong2/latlong.dart'; class PlatformMobileServices extends MobileServices { diff --git a/plugins/aves_services_huawei/lib/src/map.dart b/plugins/aves_services_huawei/lib/src/map.dart index 82d02d966..a51a0354f 100644 --- a/plugins/aves_services_huawei/lib/src/map.dart +++ b/plugins/aves_services_huawei/lib/src/map.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; -import 'package:huawei_map/map.dart'; +import 'package:huawei_map/huawei_map.dart'; import 'package:latlong2/latlong.dart' as ll; import 'package:provider/provider.dart'; @@ -182,7 +182,7 @@ class _EntryHmsMapState<T> extends State<EntryHmsMap<T>> { ...markers, if (dotLocation != null && _dotMarkerBitmap != null) Marker( - markerId: MarkerId('dot'), + markerId: const MarkerId('dot'), anchor: const Offset(.5, .5), clickable: true, icon: BitmapDescriptor.fromBytes(_dotMarkerBitmap!), diff --git a/plugins/aves_services_huawei/pubspec.lock b/plugins/aves_services_huawei/pubspec.lock index 91c859936..2fe55e115 100644 --- a/plugins/aves_services_huawei/pubspec.lock +++ b/plugins/aves_services_huawei/pubspec.lock @@ -134,10 +134,10 @@ packages: dependency: "direct main" description: name: huawei_map - sha256: "8438cfa448711b6727cf4433ffc52bc20e36fb20105608703c644a6287b96f38" + sha256: "3cee2a6fe1a8eb03782f29588df082de14b09f81c88b376017ad5afda6df2555" url: "https://pub.dev" source: hosted - version: "6.5.0+301" + version: "6.9.0+300" intl: dependency: transitive description: @@ -267,18 +267,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" + version: "1.10.0" string_scanner: dependency: transitive description: @@ -336,5 +328,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_huawei/pubspec.yaml b/plugins/aves_services_huawei/pubspec.yaml index b462c1a74..148a85401 100644 --- a/plugins/aves_services_huawei/pubspec.yaml +++ b/plugins/aves_services_huawei/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_services_none/pubspec.lock b/plugins/aves_services_none/pubspec.lock index 4a10db68e..044a6f3f6 100644 --- a/plugins/aves_services_none/pubspec.lock +++ b/plugins/aves_services_none/pubspec.lock @@ -236,10 +236,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" string_scanner: dependency: transitive description: @@ -297,5 +297,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_none/pubspec.yaml b/plugins/aves_services_none/pubspec.yaml index 691b2f7b8..4d4d7651b 100644 --- a/plugins/aves_services_none/pubspec.yaml +++ b/plugins/aves_services_none/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_ui/pubspec.lock b/plugins/aves_ui/pubspec.lock index 6e38c33fc..c656d7419 100644 --- a/plugins/aves_ui/pubspec.lock +++ b/plugins/aves_ui/pubspec.lock @@ -76,4 +76,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" diff --git a/plugins/aves_ui/pubspec.yaml b/plugins/aves_ui/pubspec.yaml index 3b715f44f..b7efcbcc0 100644 --- a/plugins/aves_ui/pubspec.yaml +++ b/plugins/aves_ui/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.4 <3.0.0" + sdk: ">=2.19.6 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_utils/pubspec.lock b/plugins/aves_utils/pubspec.lock index 6e38c33fc..c656d7419 100644 --- a/plugins/aves_utils/pubspec.lock +++ b/plugins/aves_utils/pubspec.lock @@ -76,4 +76,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" diff --git a/plugins/aves_utils/pubspec.yaml b/plugins/aves_utils/pubspec.yaml index 3828c7ab0..bdd6c1210 100644 --- a/plugins/aves_utils/pubspec.yaml +++ b/plugins/aves_utils/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: '>=2.19.4 <3.0.0' + sdk: '>=2.19.6 <3.0.0' dependencies: flutter: diff --git a/plugins/aves_video/pubspec.lock b/plugins/aves_video/pubspec.lock index 3902a0a45..35cc0e9c3 100644 --- a/plugins/aves_video/pubspec.lock +++ b/plugins/aves_video/pubspec.lock @@ -24,6 +24,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" flutter: dependency: "direct main" description: flutter @@ -83,4 +91,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.4 <3.0.0" + dart: ">=2.19.6 <3.0.0" diff --git a/plugins/aves_video/pubspec.yaml b/plugins/aves_video/pubspec.yaml index c4362c548..7bde43cf3 100644 --- a/plugins/aves_video/pubspec.yaml +++ b/plugins/aves_video/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: '>=2.19.4 <3.0.0' + sdk: '>=2.19.6 <3.0.0' dependencies: flutter: diff --git a/pubspec.lock b/pubspec.lock index 9aa79cb26..f261baca7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "98d1d33ed129b372846e862de23a0fc365745f4d7b5e786ce667fcbbb7ac5c07" + sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214 url: "https://pub.dev" source: hosted - version: "55.0.0" + version: "58.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "330d7fcbb72624f5b6d374af8b059b0ef4ba96ba5b8987f874964a1287eb617d" + sha256: f175bc1414e4edf8c5b83372c98eeabecf8353f39c9da423c2cfdf1f1f508788 url: "https://pub.dev" source: hosted - version: "1.0.18" + version: "1.1.0" analyzer: dependency: transitive description: name: analyzer - sha256: "881348aed9b0b425882c97732629a6a31093c8ff20fc4b3b03fb9d3d50a3a126" + sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27 url: "https://pub.dev" source: hosted - version: "5.7.1" + version: "5.10.0" archive: dependency: transitive description: @@ -84,6 +84,13 @@ packages: relative: true source: path version: "0.0.1" + aves_screen_state: + dependency: "direct main" + description: + path: "plugins/aves_screen_state" + relative: true + source: path + version: "0.0.1" aves_services: dependency: "direct main" description: @@ -188,10 +195,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "8875e8ed511a49f030e313656154e4bbbcef18d68dfd32eb853fac10bce48e96" + sha256: d73575bb66216738db892f72ba67dc478bd3b5490fbbcf43644b57645eabc822 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" connectivity_plus_platform_interface: dependency: transitive description: @@ -260,10 +267,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" device_info_plus_platform_interface: dependency: transitive description: @@ -276,10 +283,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: c4a508284b14ec4dda5adba2c28b2cdd34fbae1afead7e8c52cad87d51c5405b + sha256: bbebb1b7ebed819e0ec83d4abdc2a8482d934f6a85289ffc1c6acf7589fa2aad url: "https://pub.dev" source: hosted - version: "1.6.2" + version: "1.6.3" equatable: dependency: "direct main" description: @@ -342,58 +349,58 @@ packages: dependency: transitive description: name: firebase_core - sha256: "75f747cafd7cbd6c00b908e3a7aa59fc31593d46ba8165d9ee8a79e69464a394" + sha256: ed611fb8e67e43ecc7956f242cecca383d87cf71aace27287aa5dd4bdba4ac07 url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.9.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" + sha256: "0df0a064ab0cad7f8836291ca6f3272edd7b83ad5b3540478ee46a0849d8022b" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "0c1cf1f1022d2245ac117443bb95207952ca770281524d2908e323bc063fb8ff" + sha256: "347351a8f0518f3343d79a9a0690fa67ad232fc32e2ea270677791949eac792b" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics - sha256: "5410b6ab2009fc6c181ca82e16525fdcb324c5851933bfbb49246543b1005ebc" + sha256: "42cf6a137eaae7e485e6cc9794336e8e518c506b691aa6e19ff918206c535bec" url: "https://pub.dev" source: hosted - version: "3.0.17" + version: "3.1.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: bf3d3791c51a8448413b5ebd8388fa7db239a3e165db6bbd8ab58ef0e8f61906 + sha256: baa4c3d4af426d29800f0d80d165f31df4548985db151fd761346e07ed433d31 url: "https://pub.dev" source: hosted - version: "3.3.17" + version: "3.4.0" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - sha256: "607c9fdb26be84d4a5a0931ab42a7eda725372e4f5ebaa2526ab6b22ead752f9" + sha256: f0e0db8e3e47435cfbe9aa15c71b898fa218be0fc4ae409e1e42d5d5266b2c90 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: e61950ccadfb8d43ce5cdef382e8f689edc053ce6b837e277539410ecfb3b3e5 + sha256: "7058288ef97d348657ac95cea25d65a9aac181ca08387ede891fd7230ad7600f" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" floating: dependency: "direct main" description: @@ -532,10 +539,26 @@ packages: dependency: transitive description: name: google_api_availability - sha256: "1642876fa87515fd5e4074458f22d6ba4518919c5abce16baaa2878c3c555678" + sha256: "3e9548cfd991d983d11425a2436d5bd957d048c279cc9e145ffe3f36fd847385" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" + google_api_availability_android: + dependency: transitive + description: + name: google_api_availability_android + sha256: eb309bc0b435731d18f306b598e176a9afcf642089a7d7c5cbb48e393afda345 + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + google_api_availability_platform_interface: + dependency: transitive + description: + name: google_api_availability_platform_interface + sha256: "65b7da62fe5b582bb3d508628ad827d36d890710ea274766a992a56fa5420da6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" google_maps_flutter: dependency: transitive description: @@ -548,10 +571,10 @@ packages: dependency: transitive description: name: google_maps_flutter_android - sha256: a8ee18649a67750cbd477a6867a1bf9c4154c5e9f69d722c8b53a627a6d58303 + sha256: ee3c1a63983b8ba17a9a7c1233c3542fbfbc0ebf540d3aa9b8fd5162681e0219 url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" google_maps_flutter_ios: dependency: transitive description: @@ -708,10 +731,10 @@ packages: dependency: transitive description: name: markdown - sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b + sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5 url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" matcher: dependency: transitive description: @@ -813,10 +836,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "8df5ab0a481d7dc20c0e63809e90a588e496d276ba53358afc4c4443d0a00697" + sha256: cbff87676c352d97116af6dbea05aa28c4d65eb0f6d5677a520c11a69ca9a24d url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.1.0" package_info_plus_platform_interface: dependency: transitive description: @@ -894,10 +917,10 @@ packages: dependency: "direct main" description: name: pdf - sha256: "6cd57e8e6d052bd1078f18e0dc7cd6701fad6288231c1ce99d66ef5034d14e61" + sha256: "586d3debf5432e5377044754032cfa53ab45e9abf371d4865e9ad5019570e246" url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.10.1" percent_indicator: dependency: "direct main" description: @@ -926,10 +949,10 @@ packages: dependency: transitive description: name: permission_handler_apple - sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163" + sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 url: "https://pub.dev" source: hosted - version: "9.0.7" + version: "9.0.8" permission_handler_platform_interface: dependency: transitive description: @@ -1006,10 +1029,10 @@ packages: dependency: "direct main" description: name: printing - sha256: fe654363cd0114b50a0815b24e96957c7e9a60eb4e3b7ccfe71cf3f2b7114cb2 + sha256: c5c19dd852e95aa140141df13fa304f079a20c4a14a66de5275a0f811240aeec url: "https://pub.dev" source: hosted - version: "5.10.1" + version: "5.10.3" process: dependency: transitive description: @@ -1062,10 +1085,10 @@ packages: dependency: transitive description: name: screen_brightness_android - sha256: "4e4ba0c44b5c24be20030733ada0c844aa0e8f1963f5d7cd72f5b2fe54a61495" + sha256: "69231ea2cf83a627120302a82e98e739ee7e97c1077b58fd0ff0ad954e95a36e" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.0+1" screen_brightness_ios: dependency: transitive description: @@ -1098,70 +1121,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.1" - screen_state: - dependency: "direct main" - description: - name: screen_state - sha256: "39184c718baf303f26200f6b1392b12a549d88410e907e046d75594588c0df5d" - url: "https://pub.dev" - source: hosted - version: "2.0.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 + sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.1.0" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521 + sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.1.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310" + sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707" + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" shared_preferences_platform_interface: dependency: "direct dev" description: name: shared_preferences_platform_interface - sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc" + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8" + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436" + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" shelf: dependency: transitive description: @@ -1380,18 +1395,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "845530e5e05db5500c1a4c1446785d60cbd8f9bd45e21e7dd643a3273bb4bbd1" + sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411 url: "https://pub.dev" source: hosted - version: "6.0.25" + version: "6.0.27" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7ab1e5b646623d6a2537aa59d5d039f90eebef75a7c25e105f6f75de1f7750c3" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" url_launcher_linux: dependency: transitive description: @@ -1404,10 +1419,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: @@ -1468,10 +1483,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" webdriver: dependency: transitive description: @@ -1492,10 +1507,10 @@ packages: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" wkt_parser: dependency: transitive description: @@ -1529,5 +1544,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.4 <3.0.0" - flutter: ">=3.7.7" + dart: ">=2.19.6 <3.0.0" + flutter: ">=3.7.11" diff --git a/pubspec.yaml b/pubspec.yaml index e34ee4d0c..bb1e0a458 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,14 +7,14 @@ repository: https://github.com/deckerst/aves # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt -version: 1.8.4+95 +version: 1.8.5+96 publish_to: none environment: # this project bundles Flutter SDK via `flutter_wrapper` # cf https://github.com/passsy/flutter_wrapper - flutter: 3.7.7 - sdk: ">=2.19.4 <3.0.0" + flutter: 3.7.11 + sdk: ">=2.19.6 <3.0.0" # following https://github.blog/2021-09-01-improving-git-protocol-security-github/ # dependency GitHub repos should be referenced via `https://`, not `git://` @@ -35,6 +35,8 @@ dependencies: path: plugins/aves_report aves_report_platform: path: plugins/aves_report_crashlytics + aves_screen_state: + path: plugins/aves_screen_state aves_services: path: plugins/aves_services aves_services_platform: @@ -96,7 +98,6 @@ dependencies: proj4dart: provider: screen_brightness: - screen_state: # as of `shared_preferences` v2.0.18, upgrading packages downgrades `shared_preferences` to v0.5.4+6 # because its dependency `shared_preferences_windows` v2.1.4 gets removed # because its dependency `path_provider_windows` v2.1.4 gets removed diff --git a/shaders.sksl.json b/shaders.sksl.json index ca79c9285..2285ccbce 100644 --- a/shaders.sksl.json +++ b/shaders.sksl.json @@ -1 +1 @@ -{"platform":"android","name":"SM G970N","engineRevision":"1837b5be5f0f1376a1ccf383950e83a80177fb4e","data":{"HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAIBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAoAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CgAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADQAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAACfAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAAAQAAAAGQCBAMQACAAAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAHADAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFQBQU7BTXIAAAAAAAAAACAAAAAVQEAAQAAAAAQCDAEQQGAAAAAAAAAAAA4IAPAAACAAAAAAAEABYAAAAEAAAAAAAEEBQA":"CgAAAExTS1N6AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfM19TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzApICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAABAAAArAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMjsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzI7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IE1hdHJpeEVmZmVjdF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1MyKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMi5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMi5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MyLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7CgloYWxmNCBvdXRwdXRfUzI7CglvdXRwdXRfUzIgPSBDaXJjdWxhclJSZWN0X1MyKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRfUzI7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAHADAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAADgAQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAB7AgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADUAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAATQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACbAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAAZAADIAAAACU53QJEKAAAAAAMAAAAAIAAAAAAGIRDFB2XASAUAABQAAAAAAAAAAAAADUAAAAAAAEAAAAAIDEAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACrBAAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpoYWxmNCBDaXJjbGVfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMV9jMC54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxX2MwLncpIC0gMS4wKSAqIHVjaXJjbGVfUzFfYzAueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGludCgxKSA9PSBrRmlsbEFBX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCA/IHNhdHVyYXRlKGQpIDogaGFsZihkID4gMC41ID8gMSA6IDApKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShfc3JjLCBDaXJjbGVfUzFfYzAoX3NyYykpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb3ZlcmFnZV9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAsQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAAAAAAQFV5W6JEAAAAAYAAAABQEZ2AKAWAQAABAL6SYKDYAAAACAAAAAAQEGIAAAAACAWTWL3EYAAAADAAAAACADHIJJCYCAAAEAP2LRIPAAAAAIAAAAAAABTALI3F5SOAIAABQAAAAAABTUEUZMBAAAAAH5FYUXQAAAAAAAEAAAAAZMRGOQCQFQEAAAAAAAAAGARL2LXJHAAEAAAAAEAAAABSCQX5FQUHQAAAAAAAAAACAA4AAAACAAAAAAACCAYAAAAA":"CgAAAExTS1OGAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAAAAdgcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc182X1MwOwpoYWxmNCBTaW5nbGVJbnRlcnZhbENvbG9yaXplcl9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8xX2Nvb3JkcyA9IF9jb29yZHM7CglyZXR1cm4gaGFsZjQobWl4KHVzdGFydF9TMV9jMF9jMF9jMCwgdWVuZF9TMV9jMF9jMF9jMCwgaGFsZihfdG1wXzFfY29vcmRzLngpKSk7Cn0KaGFsZjQgTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMl9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfM19jb29yZHMgPSB2VHJhbnNmb3JtZWRDb29yZHNfNl9TMDsKCXJldHVybiBoYWxmNChoYWxmNChoYWxmKF90bXBfM19jb29yZHMueCkgKyAxZS0wNSwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgY29sb3JfeGZvcm1fUzFfYzAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IucmdiICo9IGNvbG9yLmE7CglyZXR1cm4gaGFsZjQoY29sb3IpOwp9CmhhbGY0IENvbG9yU3BhY2VYZm9ybV9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gY29sb3JfeGZvcm1fUzFfYzAoQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKF9pbnB1dCkpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ29sb3JTcGFjZVhmb3JtX1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAuAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAGAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAAAAAA=","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAAAAAAQFV5W6JEAAAAAYAAAABQEZ2AKAWAQAABAL6SYKDYAAAACAAAAAAQEGIAAAAACAWTWL3EYAAAADAAAAACADHIJJCYCAAAEAP2LRIPAAAAAIAAAAAAABTALI3F5SOAIAABQAAAAAABTUEUZMBAAAAAH5FYUXQAAAAAAAEAAAAAZMRGOQCQFQEAAAAAAAAAGARL2LXJHAAEAAAAAEAAAABSCQX5FQUHQAAAAAAAAAACAA4AAAABAACAAAACCAYAAAAA":"CgAAAExTS1PrAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzZfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc182X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAMMHAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc182X1MwOwpoYWxmNCBTaW5nbGVJbnRlcnZhbENvbG9yaXplcl9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8xX2Nvb3JkcyA9IF9jb29yZHM7CglyZXR1cm4gaGFsZjQobWl4KHVzdGFydF9TMV9jMF9jMF9jMCwgdWVuZF9TMV9jMF9jMF9jMCwgaGFsZihfdG1wXzFfY29vcmRzLngpKSk7Cn0KaGFsZjQgTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMl9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfM19jb29yZHMgPSB2VHJhbnNmb3JtZWRDb29yZHNfNl9TMDsKCXJldHVybiBoYWxmNChoYWxmNChoYWxmKF90bXBfM19jb29yZHMueCkgKyAxZS0wNSwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgY29sb3JfeGZvcm1fUzFfYzAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IucmdiICo9IGNvbG9yLmE7CglyZXR1cm4gaGFsZjQoY29sb3IpOwp9CmhhbGY0IENvbG9yU3BhY2VYZm9ybV9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gY29sb3JfeGZvcm1fUzFfYzAoQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKF9pbnB1dCkpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ29sb3JTcGFjZVhmb3JtX1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfUzEpICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAADEAANAAAAALHCKLMRAAAAAAAAABAAAAAGJBCFLQVBWAQAAAAAAQAAAAAMACQCAACAAAAA2AIBAEIAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAeAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMV9jMC54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzFfYzAudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzFfYzAueikgKiB1Y2lyY2xlRGF0YV9TMV9jMC53OwoJcmV0dXJuIGhhbGY0KE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53d3d3KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKENpcmNsZUJsdXJfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","EADQAAAAAEAAAAAUAABQAAQPAAABCFYMAAKAUEAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MyBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MyBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGluUG9zaXRpb24ueHkwejsKfQoAAAAAAACfAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CgAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAALAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CgAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAHMCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAAYQADAAAEAFEURUKQKAAAYAAAAAAAAIAAAABSCICWKY2FAEAAAMAAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB+BQAAY29uc3QgaW50IGtGaWxsQldfUzFfYzAgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzFfYzA7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgUmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMV9jMC56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IHNhdHVyYXRlKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxX2MwKSk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoaGFsZjQoY292ZXJhZ2UpKTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKFJlY3RfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAABQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACbBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAC2AgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADJAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAB5AgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB5AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADdAwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAEQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAB5AAAAACQHEB4XIQAQAADQAAAABAAAAAAABAEMVDOMCJKRAAAAAHAAAAAAAAAAACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAABRBQAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKaGFsZjQgQ2lyY2xlX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxX2MwLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzFfYzAudykpICogdWNpcmNsZV9TMV9jMC56KTsKCX0KCXJldHVybiBoYWxmNChoYWxmNChpbnQoMykgPT0ga0ZpbGxBQV9TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzAgPyBzYXR1cmF0ZShkKSA6IGhhbGYoZCA+IDAuNSA/IDEgOiAwKSkpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoX3NyYywgQ2lyY2xlX1MxX2MwKF9zcmMpKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAMAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABFAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAD2AAAAAAQAVSWGRIBAAADAAAAACAAAAAAQCGEIQOZLBIQAAAABQAAAAAAAAAAAAFAAMAAAABAAAAAAABBAMAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAARQUAAGNvbnN0IGludCBrRmlsbEJXX1MxX2MwID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxX2MwID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxX2MwID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IFJlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxX2MwLnh5LCBza19GcmFnQ29vcmQueHkpKSkpOwoJfQoJZWxzZSAKCXsKCQloYWxmNCBkaXN0czQgPSBzYXR1cmF0ZShoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TMV9jMCkpOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGNvdmVyYWdlKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShSZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvdmVyYWdlX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAeBQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAWXW3ZEQAAAADAAAAAGATHIBICYCAAAEBP2LBIPAAAAAIAAAAAEARRALJ3F5SMAAAABQAAAABABTUEURMBAAACAH5FYUHQAAAAAAAEAAAAAZ4RGGRCQFAEAAAAAAAAAGARP2LVJPAAAAAAAAEAAAABSKRXZFAUHQAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CgAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIIGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgMWUtMDUsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglyZXR1cm4gaGFsZjQob3V0Q29sb3IpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ2xhbXBlZEdyYWRpZW50X1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAABgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAQAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPAEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","DBAAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAIAAAAAAAAAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1NVAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludDIgY29vcmRzID0gaW50MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJaW50IHRleElkeCA9IGNvb3Jkcy54ID4+IDEzOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGNvb3Jkcy54ICYgMHgxRkZGLCBjb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAAEICAAB1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzFfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7CglpZiAodlRleEluZGV4X1MwID09IDApIAoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWVsc2UgCgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzFfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","CMRQCIAABBYAAAEIXBAAACDQMAABRAFAAAAAAAAAAAAAAAEABYAAAAEAAAAAAAEEBQAAAAA":"CgAAAExTS1MyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0MiBpbkVsbGlwc2VPZmZzZXQ7CmluIGZsb2F0NCBpbkVsbGlwc2VSYWRpaTsKb3V0IGZsb2F0MiB2RWxsaXBzZU9mZnNldHNfUzA7Cm91dCBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCXZFbGxpcHNlT2Zmc2V0c19TMCA9IGluRWxsaXBzZU9mZnNldDsKCXZFbGxpcHNlUmFkaWlfUzAgPSBpbkVsbGlwc2VSYWRpaTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAHIDAABpbiBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzX1MwOwppbiBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0MiBvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHk7CglvZmZzZXQgKj0gdkVsbGlwc2VSYWRpaV9TMC54eTsKCWZsb2F0IHRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZmxvYXQyIGdyYWQgPSAyLjAqb2Zmc2V0KnZFbGxpcHNlUmFkaWlfUzAueHk7CglmbG9hdCBncmFkX2RvdCA9IGRvdChncmFkLCBncmFkKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjE3NTVlLTM4KTsKCWZsb2F0IGludmxlbiA9IGludmVyc2VzcXJ0KGdyYWRfZG90KTsKCWZsb2F0IGVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNS10ZXN0Kmludmxlbik7CglvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHkqdkVsbGlwc2VSYWRpaV9TMC56dzsKCXRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZ3JhZCA9IDIuMCpvZmZzZXQqdkVsbGlwc2VSYWRpaV9TMC56dzsKCWdyYWRfZG90ID0gZG90KGdyYWQsIGdyYWQpOwoJaW52bGVuID0gaW52ZXJzZXNxcnQoZ3JhZF9kb3QpOwoJZWRnZUFscGhhICo9IHNhdHVyYXRlKDAuNSt0ZXN0Kmludmxlbik7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGhhbGYoZWRnZUFscGhhKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpbkVsbGlwc2VPZmZzZXQADgAAAGluRWxsaXBzZVJhZGlpAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAE0DAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAFBQATAAAAAAFAAMAAAABAAAAAAABBAMAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAABABAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgRWxsaXB0aWNhbFJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJZmxvYXQyIFogPSBkeHkgKiB1aW52UmFkaWlYWV9TMS54eTsKCWhhbGYgaW1wbGljaXQgPSBoYWxmKGRvdChaLCBkeHkpIC0gMS4wKTsKCWhhbGYgZ3JhZF9kb3QgPSBoYWxmKDQuMCAqIGRvdChaLCBaKSk7CglncmFkX2RvdCA9IG1heChncmFkX2RvdCwgMS4wZS00KTsKCWhhbGYgYXBwcm94X2Rpc3QgPSBpbXBsaWNpdCAqIGhhbGYoaW52ZXJzZXNxcnQoZ3JhZF9kb3QpKTsKCWhhbGYgYWxwaGEgPSBjbGFtcCgwLjUgKyBhcHByb3hfZGlzdCwgMC4wLCAxLjApOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRWxsaXB0aWNhbFJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAAAAAA=="}} \ No newline at end of file +{"platform":"android","name":"SM G970N","engineRevision":"1a65d409c7a1438a34d21b60bf30a6fd5db59314","data":{"HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAACfAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAAYQADAAAEAFEURUKQKAAAYAAAAAAAAIAAAABSCICWKY2FAEAAAMAAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB+BQAAY29uc3QgaW50IGtGaWxsQldfUzFfYzAgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzFfYzA7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgUmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMV9jMC56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IHNhdHVyYXRlKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxX2MwKSk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoaGFsZjQoY292ZXJhZ2UpKTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKFJlY3RfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAWXW3ZEQAAAADAAAAAGATHIBICYCAAAEBP2LBIPAAAAAIAAAAAEARRALJ3F5SMAAAABQAAAABABTUEURMBAAACAH5FYUHQAAAAAAAEAAAAAZ4RGGRCQFAEAAAAAAAAAGARP2LVJPAAAAAAAAEAAAABSKRXZFAUHQAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CgAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIIGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgMWUtMDUsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglyZXR1cm4gaGFsZjQob3V0Q29sb3IpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ2xhbXBlZEdyYWRpZW50X1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAD2AAAAAAQAVSWGRIBAAADAAAAACAAAAAAQCGEIQOZLBIQAAAABQAAAAAAAAAAAAFAAMAAAABAAAAAAABBAMAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAARQUAAGNvbnN0IGludCBrRmlsbEJXX1MxX2MwID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxX2MwID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxX2MwID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IFJlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxX2MwLnh5LCBza19GcmFnQ29vcmQueHkpKSkpOwoJfQoJZWxzZSAKCXsKCQloYWxmNCBkaXN0czQgPSBzYXR1cmF0ZShoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TMV9jMCkpOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGNvdmVyYWdlKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShSZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvdmVyYWdlX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFQBQU7BTXIAAAAAAAAAACAAAAAVQEAAQAAAAAQCDAEQQGAAAAAAAAAAAA4IAPAAACAAAAAAAEABYAAAAEAAAAAAAEEBQA":"CgAAAExTS1N6AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfM19TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzApICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAABAAAArAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMjsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzI7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IE1hdHJpeEVmZmVjdF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1MyKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMi5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMi5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MyLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7CgloYWxmNCBvdXRwdXRfUzI7CglvdXRwdXRfUzIgPSBDaXJjdWxhclJSZWN0X1MyKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRfUzI7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAIBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABFAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADJAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADUAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAAZAADIAAAACU53QJEKAAAAAAMAAAAAIAAAAAAGIRDFB2XASAUAABQAAAAAAAAAAAAADUAAAAAAAEAAAAAIDEAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACrBAAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpoYWxmNCBDaXJjbGVfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMV9jMC54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxX2MwLncpIC0gMS4wKSAqIHVjaXJjbGVfUzFfYzAueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGludCgxKSA9PSBrRmlsbEFBX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCA/IHNhdHVyYXRlKGQpIDogaGFsZihkID4gMC41ID8gMSA6IDApKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShfc3JjLCBDaXJjbGVfUzFfYzAoX3NyYykpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb3ZlcmFnZV9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAB5AgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB5AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CgAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAALAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACbBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAAAAAA==","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAQAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CgAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAHMCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAEQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAB7AgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAC2AgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAAAAAAQFV5W6JEAAAAAYAAAABQEZ2AKAWAQAABAL6SYKDYAAAACAAAAAAQEGIAAAAACAWTWL3EYAAAADAAAAACADHIJJCYCAAAEAP2LRIPAAAAAIAAAAAAABTALI3F5SOAIAABQAAAAAABTUEUZMBAAAAAH5FYUXQAAAAAAAEAAAAAZMRGOQCQFQEAAAAAAAAAGARL2LXJHAAEAAAAAEAAAABSCQX5FQUHQAAAAAAAAAACAA4AAAABAACAAAACCAYAAAAA":"CgAAAExTS1PrAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzZfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc182X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAMMHAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc182X1MwOwpoYWxmNCBTaW5nbGVJbnRlcnZhbENvbG9yaXplcl9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8xX2Nvb3JkcyA9IF9jb29yZHM7CglyZXR1cm4gaGFsZjQobWl4KHVzdGFydF9TMV9jMF9jMF9jMCwgdWVuZF9TMV9jMF9jMF9jMCwgaGFsZihfdG1wXzFfY29vcmRzLngpKSk7Cn0KaGFsZjQgTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMl9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfM19jb29yZHMgPSB2VHJhbnNmb3JtZWRDb29yZHNfNl9TMDsKCXJldHVybiBoYWxmNChoYWxmNChoYWxmKF90bXBfM19jb29yZHMueCkgKyAxZS0wNSwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgY29sb3JfeGZvcm1fUzFfYzAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IucmdiICo9IGNvbG9yLmE7CglyZXR1cm4gaGFsZjQoY29sb3IpOwp9CmhhbGY0IENvbG9yU3BhY2VYZm9ybV9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gY29sb3JfeGZvcm1fUzFfYzAoQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKF9pbnB1dCkpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ29sb3JTcGFjZVhmb3JtX1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfUzEpICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAAAAAA=","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAGAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAsQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPAEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAABQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAAAAAA=","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADdAwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAuAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACbAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","EADQAAAAAEAAAAAUAABQAAQPAAABCFYMAAKAUEAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MyBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MyBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGluUG9zaXRpb24ueHkwejsKfQoAAAAAAACfAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAMAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CgAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADQAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAATQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAA3AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAADgAQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAADEAANAAAAALHCKLMRAAAAAAAAABAAAAAGJBCFLQVBWAQAAAAAAQAAAAAMACQCAACAAAAA2AIBAEIAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAeAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMV9jMC54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzFfYzAudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzFfYzAueikgKiB1Y2lyY2xlRGF0YV9TMV9jMC53OwoJcmV0dXJuIGhhbGY0KE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53d3d3KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKENpcmNsZUJsdXJfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAABgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAE0DAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAHADAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAoAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","CMRQCIAABBYAAAEIXBAAACDQMAABRAFAAAAAAAAAAAAAAAEABYAAAAEAAAAAAAEEBQAAAAA":"CgAAAExTS1MyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0MiBpbkVsbGlwc2VPZmZzZXQ7CmluIGZsb2F0NCBpbkVsbGlwc2VSYWRpaTsKb3V0IGZsb2F0MiB2RWxsaXBzZU9mZnNldHNfUzA7Cm91dCBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCXZFbGxpcHNlT2Zmc2V0c19TMCA9IGluRWxsaXBzZU9mZnNldDsKCXZFbGxpcHNlUmFkaWlfUzAgPSBpbkVsbGlwc2VSYWRpaTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAHIDAABpbiBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzX1MwOwppbiBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0MiBvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHk7CglvZmZzZXQgKj0gdkVsbGlwc2VSYWRpaV9TMC54eTsKCWZsb2F0IHRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZmxvYXQyIGdyYWQgPSAyLjAqb2Zmc2V0KnZFbGxpcHNlUmFkaWlfUzAueHk7CglmbG9hdCBncmFkX2RvdCA9IGRvdChncmFkLCBncmFkKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjE3NTVlLTM4KTsKCWZsb2F0IGludmxlbiA9IGludmVyc2VzcXJ0KGdyYWRfZG90KTsKCWZsb2F0IGVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNS10ZXN0Kmludmxlbik7CglvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHkqdkVsbGlwc2VSYWRpaV9TMC56dzsKCXRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZ3JhZCA9IDIuMCpvZmZzZXQqdkVsbGlwc2VSYWRpaV9TMC56dzsKCWdyYWRfZG90ID0gZG90KGdyYWQsIGdyYWQpOwoJaW52bGVuID0gaW52ZXJzZXNxcnQoZ3JhZF9kb3QpOwoJZWRnZUFscGhhICo9IHNhdHVyYXRlKDAuNSt0ZXN0Kmludmxlbik7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGhhbGYoZWRnZUFscGhhKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpbkVsbGlwc2VPZmZzZXQADgAAAGluRWxsaXBzZVJhZGlpAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAB5AAAAACQHEB4XIQAQAADQAAAABAAAAAAABAEMVDOMCJKRAAAAAHAAAAAAAAAAACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAABRBQAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKaGFsZjQgQ2lyY2xlX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxX2MwLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzFfYzAudykpICogdWNpcmNsZV9TMV9jMC56KTsKCX0KCXJldHVybiBoYWxmNChoYWxmNChpbnQoMykgPT0ga0ZpbGxBQV9TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzAgPyBzYXR1cmF0ZShkKSA6IGhhbGYoZCA+IDAuNSA/IDEgOiAwKSkpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoX3NyYywgQ2lyY2xlX1MxX2MwKF9zcmMpKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAeBQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA"}} \ No newline at end of file diff --git a/test/fake/android_app_service.dart b/test/fake/android_app_service.dart index 436d767e9..0cd007280 100644 --- a/test/fake/android_app_service.dart +++ b/test/fake/android_app_service.dart @@ -1,9 +1,9 @@ -import 'package:aves/services/android_app_service.dart'; -import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/model/apps.dart'; +import 'package:aves/services/app_service.dart'; import 'package:flutter/foundation.dart'; import 'package:test/fake.dart'; -class FakeAndroidAppService extends Fake implements AndroidAppService { +class FakeAppService extends Fake implements AppService { @override Future<Set<Package>> getPackages() => SynchronousFuture({}); } diff --git a/test/fake/storage_service.dart b/test/fake/storage_service.dart index e74eacdcd..6c90bdb55 100644 --- a/test/fake/storage_service.dart +++ b/test/fake/storage_service.dart @@ -1,5 +1,5 @@ +import 'package:aves_model/aves_model.dart'; import 'package:aves/services/storage_service.dart'; -import 'package:aves/utils/android_file_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:test/fake.dart'; diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index ecc7127af..a8249b240 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -1,6 +1,5 @@ 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'; @@ -13,7 +12,7 @@ import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/media_store_source.dart'; -import 'package:aves/services/android_app_service.dart'; +import 'package:aves/services/app_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/device_service.dart'; import 'package:aves/services/media/media_fetch_service.dart'; @@ -22,6 +21,7 @@ import 'package:aves/services/metadata/metadata_fetch_service.dart'; import 'package:aves/services/storage_service.dart'; import 'package:aves/services/window_service.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:aves_report/aves_report.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -59,7 +59,7 @@ void main() { getIt.registerLazySingleton<AvesAvailability>(FakeAvesAvailability.new); getIt.registerLazySingleton<MetadataDb>(FakeMetadataDb.new); - getIt.registerLazySingleton<AndroidAppService>(FakeAndroidAppService.new); + getIt.registerLazySingleton<AppService>(FakeAppService.new); getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new); getIt.registerLazySingleton<MediaFetchService>(FakeMediaFetchService.new); getIt.registerLazySingleton<MediaStoreService>(FakeMediaStoreService.new); diff --git a/test/utils/xmp_utils_test.dart b/test/utils/xmp_utils_test.dart index 84197b9c1..0374f1467 100644 --- a/test/utils/xmp_utils_test.dart +++ b/test/utils/xmp_utils_test.dart @@ -1,4 +1,5 @@ import 'package:aves/model/entry/extensions/metadata_edition.dart'; +import 'package:aves/ref/metadata/xmp.dart'; import 'package:aves/utils/xmp_utils.dart'; import 'package:test/test.dart'; import 'package:xml/xml.dart'; @@ -16,9 +17,9 @@ void main() { List<XmlNode> _getDescriptions(String xmpString) { final xmpDoc = XmlDocument.parse(xmpString); final root = xmpDoc.rootElement; - final rdf = root.getElement(XMP.rdfRoot, namespace: Namespaces.rdf); + final rdf = root.getElement(XmpElements.rdfRoot, namespace: XmpNamespaces.rdf); return rdf!.children.where((node) { - return node is XmlElement && node.name.local == XMP.rdfDescription && node.name.namespaceUri == Namespaces.rdf; + return node is XmlElement && node.name.local == XmpElements.rdfDescription && node.name.namespaceUri == XmpNamespaces.rdf; }).toList(); } @@ -133,9 +134,9 @@ void main() { '''; test('Get string', () async { - expect(XMP.getString(_getDescriptions(inRatingAttribute), XMP.xmpRating, namespace: Namespaces.xmp), '5'); - expect(XMP.getString(_getDescriptions(inRatingElement), XMP.xmpRating, namespace: Namespaces.xmp), '5'); - expect(XMP.getString(_getDescriptions(inSubjects), XMP.xmpRating, namespace: Namespaces.xmp), null); + expect(XMP.getString(_getDescriptions(inRatingAttribute), XmpElements.xmpRating, namespace: XmpNamespaces.xmp), '5'); + expect(XMP.getString(_getDescriptions(inRatingElement), XmpElements.xmpRating, namespace: XmpNamespaces.xmp), '5'); + expect(XMP.getString(_getDescriptions(inSubjects), XmpElements.xmpRating, namespace: XmpNamespaces.xmp), null); }); test('Set tags without existing XMP', () async { diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index abbbf3c3c..12a9c1939 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -1,12 +1,11 @@ import 'package:aves/main_play.dart' as app; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/settings/defaults.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test_driver/driver_shaders.dart b/test_driver/driver_shaders.dart index 384672149..5c84a6e0b 100644 --- a/test_driver/driver_shaders.dart +++ b/test_driver/driver_shaders.dart @@ -2,10 +2,9 @@ import 'dart:ui'; import 'package:aves/main_play.dart' as app; import 'package:aves/model/settings/defaults.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/enums/enums.dart'; import 'package:aves_map/src/style.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/untranslated.json b/untranslated.json index 0bffff390..ae0b5d5e8 100644 --- a/untranslated.json +++ b/untranslated.json @@ -24,6 +24,7 @@ "chipActionUnpin", "chipActionRename", "chipActionSetCover", + "chipActionShowCountryStates", "chipActionCreateAlbum", "chipActionCreateVault", "chipActionConfigureVault", @@ -62,6 +63,8 @@ "videoActionSelectStreams", "videoActionSetSpeed", "viewerActionSettings", + "viewerActionLock", + "viewerActionUnlock", "slideshowActionResume", "slideshowActionShowInCollection", "entryInfoActionEditDate", @@ -378,6 +381,8 @@ "newFilterBanner", "countryPageTitle", "countryEmpty", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", "tagPageTitle", @@ -388,6 +393,7 @@ "searchDateSectionTitle", "searchAlbumsSectionTitle", "searchCountriesSectionTitle", + "searchStatesSectionTitle", "searchPlacesSectionTitle", "searchTagsSectionTitle", "searchRatingSectionTitle", @@ -443,6 +449,8 @@ "settingsCollectionQuickActionTabSelecting", "settingsCollectionBrowsingQuickActionEditorBanner", "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerSectionTitle", "settingsViewerGestureSideTapNext", "settingsViewerUseCutout", @@ -558,6 +566,7 @@ "statsPageTitle", "statsWithGps", "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", "statsTopPlacesSectionTitle", "statsTopTagsSectionTitle", "statsTopAlbumsSectionTitle", @@ -600,11 +609,13 @@ "viewerInfoSearchSuggestionRights", "wallpaperUseScrollEffect", "tagEditorPageTitle", - "tagEditorPageNewTagFieldLabel" + "tagEditorPageNewTagFieldLabel", + "tagPlaceholderState" ], "ckb": [ "chipActionGoToPlacePage", + "chipActionShowCountryStates", "entryActionRotateCCW", "entryActionRotateCW", "entryActionFlip", @@ -620,6 +631,8 @@ "videoActionSelectStreams", "videoActionSetSpeed", "viewerActionSettings", + "viewerActionLock", + "viewerActionUnlock", "slideshowActionResume", "slideshowActionShowInCollection", "entryInfoActionEditDate", @@ -936,6 +949,8 @@ "newFilterBanner", "countryPageTitle", "countryEmpty", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", "tagPageTitle", @@ -946,6 +961,7 @@ "searchDateSectionTitle", "searchAlbumsSectionTitle", "searchCountriesSectionTitle", + "searchStatesSectionTitle", "searchPlacesSectionTitle", "searchTagsSectionTitle", "searchRatingSectionTitle", @@ -1001,6 +1017,8 @@ "settingsCollectionQuickActionTabSelecting", "settingsCollectionBrowsingQuickActionEditorBanner", "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerSectionTitle", "settingsViewerGestureSideTapNext", "settingsViewerUseCutout", @@ -1116,6 +1134,7 @@ "statsPageTitle", "statsWithGps", "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", "statsTopPlacesSectionTitle", "statsTopTagsSectionTitle", "statsTopAlbumsSectionTitle", @@ -1163,6 +1182,7 @@ "tagEditorSectionRecent", "tagEditorSectionPlaceholders", "tagPlaceholderCountry", + "tagPlaceholderState", "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", @@ -1174,71 +1194,18 @@ "filePickerUseThisFolder" ], - "cs": [ - "settingsVideoEnablePip", - "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle" - ], - - "de": [ - "chipActionGoToPlacePage", - "chipActionLock", - "chipActionCreateVault", - "chipActionConfigureVault", - "albumTierVaults", - "lengthUnitPixel", - "lengthUnitPercent", - "vaultLockTypePattern", - "vaultLockTypePin", - "vaultLockTypePassword", - "settingsVideoEnablePip", - "newVaultWarningDialogMessage", - "newVaultDialogTitle", - "configureVaultDialogTitle", - "vaultDialogLockModeWhenScreenOff", - "vaultDialogLockTypeLabel", - "patternDialogEnter", - "patternDialogConfirm", - "pinDialogEnter", - "pinDialogConfirm", - "passwordDialogEnter", - "passwordDialogConfirm", - "authenticateToConfigureVault", - "authenticateToUnlockVault", - "vaultBinUsageDialogMessage", - "exportEntryDialogWriteMetadata", - "drawerPlacePage", - "placePageTitle", - "placeEmpty", - "settingsConfirmationVaultDataLoss", - "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle", - "settingsDisablingBinWarningDialogMessage" - ], - - "el": [ - "chipActionGoToPlacePage", - "vaultLockTypePattern", - "settingsVideoEnablePip", - "patternDialogEnter", - "patternDialogConfirm", - "exportEntryDialogWriteMetadata", - "drawerPlacePage", - "placePageTitle", - "placeEmpty", - "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle" - ], - "fa": [ "clearTooltip", "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", "videoActionPause", "videoActionPlay", "videoActionSelectStreams", + "viewerActionLock", + "viewerActionUnlock", "slideshowActionResume", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", @@ -1472,6 +1439,8 @@ "newFilterBanner", "countryPageTitle", "countryEmpty", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", "tagPageTitle", @@ -1482,6 +1451,7 @@ "searchDateSectionTitle", "searchAlbumsSectionTitle", "searchCountriesSectionTitle", + "searchStatesSectionTitle", "searchPlacesSectionTitle", "searchTagsSectionTitle", "searchRatingSectionTitle", @@ -1533,6 +1503,8 @@ "settingsCollectionQuickActionTabSelecting", "settingsCollectionBrowsingQuickActionEditorBanner", "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerSectionTitle", "settingsViewerGestureSideTapNext", "settingsViewerUseCutout", @@ -1648,6 +1620,7 @@ "statsPageTitle", "statsWithGps", "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", "statsTopPlacesSectionTitle", "statsTopTagsSectionTitle", "statsTopAlbumsSectionTitle", @@ -1693,6 +1666,7 @@ "tagEditorSectionRecent", "tagEditorSectionPlaceholders", "tagPlaceholderCountry", + "tagPlaceholderState", "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", @@ -1708,10 +1682,13 @@ "columnCount", "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", "entryActionShareImageOnly", "entryActionShareVideoOnly", + "viewerActionLock", + "viewerActionUnlock", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", @@ -1972,6 +1949,8 @@ "newFilterBanner", "countryPageTitle", "countryEmpty", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", "tagPageTitle", @@ -1982,6 +1961,7 @@ "searchDateSectionTitle", "searchAlbumsSectionTitle", "searchCountriesSectionTitle", + "searchStatesSectionTitle", "searchPlacesSectionTitle", "searchTagsSectionTitle", "searchRatingSectionTitle", @@ -2037,6 +2017,8 @@ "settingsCollectionQuickActionTabSelecting", "settingsCollectionBrowsingQuickActionEditorBanner", "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerSectionTitle", "settingsViewerGestureSideTapNext", "settingsViewerUseCutout", @@ -2152,6 +2134,7 @@ "statsPageTitle", "statsWithGps", "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", "statsTopPlacesSectionTitle", "statsTopTagsSectionTitle", "statsTopAlbumsSectionTitle", @@ -2199,6 +2182,7 @@ "tagEditorSectionRecent", "tagEditorSectionPlaceholders", "tagPlaceholderCountry", + "tagPlaceholderState", "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", @@ -2253,6 +2237,7 @@ "chipActionUnpin", "chipActionRename", "chipActionSetCover", + "chipActionShowCountryStates", "chipActionCreateAlbum", "chipActionCreateVault", "chipActionConfigureVault", @@ -2291,6 +2276,8 @@ "videoActionSelectStreams", "videoActionSetSpeed", "viewerActionSettings", + "viewerActionLock", + "viewerActionUnlock", "slideshowActionResume", "slideshowActionShowInCollection", "entryInfoActionEditDate", @@ -2607,6 +2594,8 @@ "newFilterBanner", "countryPageTitle", "countryEmpty", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", "tagPageTitle", @@ -2617,6 +2606,7 @@ "searchDateSectionTitle", "searchAlbumsSectionTitle", "searchCountriesSectionTitle", + "searchStatesSectionTitle", "searchPlacesSectionTitle", "searchTagsSectionTitle", "searchRatingSectionTitle", @@ -2672,6 +2662,8 @@ "settingsCollectionQuickActionTabSelecting", "settingsCollectionBrowsingQuickActionEditorBanner", "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerSectionTitle", "settingsViewerGestureSideTapNext", "settingsViewerUseCutout", @@ -2787,6 +2779,7 @@ "statsPageTitle", "statsWithGps", "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", "statsTopPlacesSectionTitle", "statsTopTagsSectionTitle", "statsTopAlbumsSectionTitle", @@ -2834,6 +2827,1189 @@ "tagEditorSectionRecent", "tagEditorSectionPlaceholders", "tagPlaceholderCountry", + "tagPlaceholderState", + "tagPlaceholderPlace", + "panoramaEnableSensorControl", + "panoramaDisableSensorControl", + "sourceViewerPageTitle", + "filePickerShowHiddenFiles", + "filePickerDoNotShowHiddenFiles", + "filePickerOpenFrom", + "filePickerNoItems", + "filePickerUseThisFolder" + ], + + "hi": [ + "resetTooltip", + "saveTooltip", + "pickTooltip", + "doubleBackExitMessage", + "doNotAskAgain", + "sourceStateLoading", + "sourceStateCataloguing", + "sourceStateLocatingCountries", + "sourceStateLocatingPlaces", + "chipActionDelete", + "chipActionGoToAlbumPage", + "chipActionGoToCountryPage", + "chipActionGoToPlacePage", + "chipActionGoToTagPage", + "chipActionFilterOut", + "chipActionFilterIn", + "chipActionHide", + "chipActionLock", + "chipActionPin", + "chipActionUnpin", + "chipActionRename", + "chipActionSetCover", + "chipActionShowCountryStates", + "chipActionCreateAlbum", + "chipActionCreateVault", + "chipActionConfigureVault", + "entryActionCopyToClipboard", + "entryActionDelete", + "entryActionConvert", + "entryActionExport", + "entryActionInfo", + "entryActionRename", + "entryActionRestore", + "entryActionRotateCCW", + "entryActionRotateCW", + "entryActionFlip", + "entryActionPrint", + "entryActionShare", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", + "entryActionViewSource", + "entryActionShowGeoTiffOnMap", + "entryActionConvertMotionPhotoToStillImage", + "entryActionViewMotionPhotoVideo", + "entryActionEdit", + "entryActionOpen", + "entryActionSetAs", + "entryActionOpenMap", + "entryActionRotateScreen", + "entryActionAddFavourite", + "entryActionRemoveFavourite", + "videoActionCaptureFrame", + "videoActionMute", + "videoActionUnmute", + "videoActionPause", + "videoActionPlay", + "videoActionReplay10", + "videoActionSkip10", + "videoActionSelectStreams", + "videoActionSetSpeed", + "viewerActionSettings", + "viewerActionLock", + "viewerActionUnlock", + "slideshowActionResume", + "slideshowActionShowInCollection", + "entryInfoActionEditDate", + "entryInfoActionEditLocation", + "entryInfoActionEditTitleDescription", + "entryInfoActionEditRating", + "entryInfoActionEditTags", + "entryInfoActionRemoveMetadata", + "entryInfoActionExportMetadata", + "entryInfoActionRemoveLocation", + "filterAspectRatioLandscapeLabel", + "filterAspectRatioPortraitLabel", + "filterBinLabel", + "filterFavouriteLabel", + "filterNoDateLabel", + "filterNoAddressLabel", + "filterLocatedLabel", + "filterNoLocationLabel", + "filterNoRatingLabel", + "filterTaggedLabel", + "filterNoTagLabel", + "filterNoTitleLabel", + "filterOnThisDayLabel", + "filterRecentlyAddedLabel", + "filterRatingRejectedLabel", + "filterTypeAnimatedLabel", + "filterTypeMotionPhotoLabel", + "filterTypePanoramaLabel", + "filterTypeRawLabel", + "filterTypeSphericalVideoLabel", + "filterTypeGeotiffLabel", + "filterMimeImageLabel", + "filterMimeVideoLabel", + "accessibilityAnimationsRemove", + "accessibilityAnimationsKeep", + "albumTierNew", + "albumTierPinned", + "albumTierSpecial", + "albumTierApps", + "albumTierVaults", + "albumTierRegular", + "coordinateFormatDms", + "coordinateFormatDecimal", + "coordinateDms", + "coordinateDmsNorth", + "coordinateDmsSouth", + "coordinateDmsEast", + "coordinateDmsWest", + "displayRefreshRatePreferHighest", + "displayRefreshRatePreferLowest", + "keepScreenOnNever", + "keepScreenOnVideoPlayback", + "keepScreenOnViewerOnly", + "keepScreenOnAlways", + "lengthUnitPixel", + "lengthUnitPercent", + "mapStyleGoogleNormal", + "mapStyleGoogleHybrid", + "mapStyleGoogleTerrain", + "mapStyleHuaweiNormal", + "mapStyleHuaweiTerrain", + "mapStyleOsmHot", + "mapStyleStamenToner", + "mapStyleStamenWatercolor", + "nameConflictStrategyRename", + "nameConflictStrategyReplace", + "nameConflictStrategySkip", + "subtitlePositionTop", + "subtitlePositionBottom", + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "unitSystemMetric", + "unitSystemImperial", + "vaultLockTypePattern", + "vaultLockTypePin", + "vaultLockTypePassword", + "settingsVideoEnablePip", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", + "videoControlsNone", + "videoLoopModeNever", + "videoLoopModeShortOnly", + "videoLoopModeAlways", + "videoPlaybackSkip", + "videoPlaybackMuted", + "videoPlaybackWithSound", + "viewerTransitionSlide", + "viewerTransitionParallax", + "viewerTransitionFade", + "viewerTransitionZoomIn", + "viewerTransitionNone", + "wallpaperTargetHome", + "wallpaperTargetLock", + "wallpaperTargetHomeLock", + "widgetDisplayedItemRandom", + "widgetDisplayedItemMostRecent", + "widgetOpenPageHome", + "widgetOpenPageCollection", + "widgetOpenPageViewer", + "storageVolumeDescriptionFallbackPrimary", + "storageVolumeDescriptionFallbackNonPrimary", + "rootDirectoryDescription", + "otherDirectoryDescription", + "storageAccessDialogMessage", + "restrictedAccessDialogMessage", + "notEnoughSpaceDialogMessage", + "missingSystemFilePickerDialogMessage", + "unsupportedTypeDialogMessage", + "nameConflictDialogSingleSourceMessage", + "nameConflictDialogMultipleSourceMessage", + "addShortcutDialogLabel", + "addShortcutButtonLabel", + "noMatchingAppDialogMessage", + "binEntriesConfirmationDialogMessage", + "deleteEntriesConfirmationDialogMessage", + "moveUndatedConfirmationDialogMessage", + "moveUndatedConfirmationDialogSetDate", + "videoResumeDialogMessage", + "videoStartOverButtonLabel", + "videoResumeButtonLabel", + "setCoverDialogLatest", + "setCoverDialogAuto", + "setCoverDialogCustom", + "hideFilterConfirmationDialogMessage", + "newAlbumDialogTitle", + "newAlbumDialogNameLabel", + "newAlbumDialogNameLabelAlreadyExistsHelper", + "newAlbumDialogStorageLabel", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "patternDialogEnter", + "patternDialogConfirm", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "renameAlbumDialogLabel", + "renameAlbumDialogLabelAlreadyExistsHelper", + "renameEntrySetPageTitle", + "renameEntrySetPagePatternFieldLabel", + "renameEntrySetPageInsertTooltip", + "renameEntrySetPagePreviewSectionTitle", + "renameProcessorCounter", + "renameProcessorName", + "deleteSingleAlbumConfirmationDialogMessage", + "deleteMultiAlbumConfirmationDialogMessage", + "exportEntryDialogFormat", + "exportEntryDialogWidth", + "exportEntryDialogHeight", + "exportEntryDialogWriteMetadata", + "renameEntryDialogLabel", + "editEntryDialogCopyFromItem", + "editEntryDialogTargetFieldsHeader", + "editEntryDateDialogTitle", + "editEntryDateDialogSetCustom", + "editEntryDateDialogCopyField", + "editEntryDateDialogExtractFromTitle", + "editEntryDateDialogShift", + "editEntryDateDialogSourceFileModifiedDate", + "durationDialogHours", + "durationDialogMinutes", + "durationDialogSeconds", + "editEntryLocationDialogTitle", + "editEntryLocationDialogSetCustom", + "editEntryLocationDialogChooseOnMap", + "editEntryLocationDialogLatitude", + "editEntryLocationDialogLongitude", + "locationPickerUseThisLocationButton", + "editEntryRatingDialogTitle", + "removeEntryMetadataDialogTitle", + "removeEntryMetadataDialogMore", + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage", + "videoSpeedDialogLabel", + "videoStreamSelectionDialogVideo", + "videoStreamSelectionDialogAudio", + "videoStreamSelectionDialogText", + "videoStreamSelectionDialogOff", + "videoStreamSelectionDialogTrack", + "videoStreamSelectionDialogNoSelection", + "genericSuccessFeedback", + "genericFailureFeedback", + "genericDangerWarningDialogMessage", + "tooManyItemsErrorDialogMessage", + "menuActionConfigureView", + "menuActionSelect", + "menuActionSelectAll", + "menuActionSelectNone", + "menuActionMap", + "menuActionSlideshow", + "menuActionStats", + "viewDialogSortSectionTitle", + "viewDialogGroupSectionTitle", + "viewDialogLayoutSectionTitle", + "viewDialogReverseSortOrder", + "tileLayoutMosaic", + "tileLayoutGrid", + "tileLayoutList", + "coverDialogTabCover", + "coverDialogTabApp", + "coverDialogTabColor", + "appPickDialogTitle", + "appPickDialogNone", + "aboutPageTitle", + "aboutLinkLicense", + "aboutLinkPolicy", + "aboutBugSectionTitle", + "aboutBugSaveLogInstruction", + "aboutBugCopyInfoInstruction", + "aboutBugCopyInfoButton", + "aboutBugReportInstruction", + "aboutBugReportButton", + "aboutCreditsSectionTitle", + "aboutCreditsWorldAtlas1", + "aboutCreditsWorldAtlas2", + "aboutTranslatorsSectionTitle", + "aboutLicensesSectionTitle", + "aboutLicensesBanner", + "aboutLicensesAndroidLibrariesSectionTitle", + "aboutLicensesFlutterPluginsSectionTitle", + "aboutLicensesFlutterPackagesSectionTitle", + "aboutLicensesDartPackagesSectionTitle", + "aboutLicensesShowAllButtonLabel", + "policyPageTitle", + "collectionPageTitle", + "collectionPickPageTitle", + "collectionSelectPageTitle", + "collectionActionShowTitleSearch", + "collectionActionHideTitleSearch", + "collectionActionAddShortcut", + "collectionActionEmptyBin", + "collectionActionCopy", + "collectionActionMove", + "collectionActionRescan", + "collectionActionEdit", + "collectionSearchTitlesHintText", + "collectionGroupAlbum", + "collectionGroupMonth", + "collectionGroupDay", + "collectionGroupNone", + "sectionUnknown", + "dateToday", + "dateYesterday", + "dateThisMonth", + "collectionDeleteFailureFeedback", + "collectionCopyFailureFeedback", + "collectionMoveFailureFeedback", + "collectionRenameFailureFeedback", + "collectionEditFailureFeedback", + "collectionExportFailureFeedback", + "collectionCopySuccessFeedback", + "collectionMoveSuccessFeedback", + "collectionRenameSuccessFeedback", + "collectionEditSuccessFeedback", + "collectionEmptyFavourites", + "collectionEmptyVideos", + "collectionEmptyImages", + "collectionEmptyGrantAccessButtonLabel", + "collectionSelectSectionTooltip", + "collectionDeselectSectionTooltip", + "drawerAboutButton", + "drawerSettingsButton", + "drawerCollectionAll", + "drawerCollectionFavourites", + "drawerCollectionImages", + "drawerCollectionVideos", + "drawerCollectionAnimated", + "drawerCollectionMotionPhotos", + "drawerCollectionPanoramas", + "drawerCollectionRaws", + "drawerCollectionSphericalVideos", + "drawerAlbumPage", + "drawerCountryPage", + "drawerPlacePage", + "drawerTagPage", + "sortByDate", + "sortByName", + "sortByItemCount", + "sortBySize", + "sortByAlbumFileName", + "sortByRating", + "sortOrderNewestFirst", + "sortOrderOldestFirst", + "sortOrderAtoZ", + "sortOrderZtoA", + "sortOrderHighestFirst", + "sortOrderLowestFirst", + "sortOrderLargestFirst", + "sortOrderSmallestFirst", + "albumGroupTier", + "albumGroupType", + "albumGroupVolume", + "albumGroupNone", + "albumMimeTypeMixed", + "albumPickPageTitleCopy", + "albumPickPageTitleExport", + "albumPickPageTitleMove", + "albumPickPageTitlePick", + "albumCamera", + "albumDownload", + "albumScreenshots", + "albumScreenRecordings", + "albumVideoCaptures", + "albumPageTitle", + "albumEmpty", + "createAlbumButtonLabel", + "newFilterBanner", + "countryPageTitle", + "countryEmpty", + "statePageTitle", + "stateEmpty", + "placePageTitle", + "placeEmpty", + "tagPageTitle", + "tagEmpty", + "binPageTitle", + "searchCollectionFieldHint", + "searchRecentSectionTitle", + "searchDateSectionTitle", + "searchAlbumsSectionTitle", + "searchCountriesSectionTitle", + "searchStatesSectionTitle", + "searchPlacesSectionTitle", + "searchTagsSectionTitle", + "searchRatingSectionTitle", + "searchMetadataSectionTitle", + "settingsPageTitle", + "settingsSystemDefault", + "settingsDefault", + "settingsDisabled", + "settingsModificationWarningDialogMessage", + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsActionExport", + "settingsActionExportDialogTitle", + "settingsActionImport", + "settingsActionImportDialogTitle", + "appExportCovers", + "appExportFavourites", + "appExportSettings", + "settingsNavigationSectionTitle", + "settingsHomeTile", + "settingsHomeDialogTitle", + "settingsShowBottomNavigationBar", + "settingsKeepScreenOnTile", + "settingsKeepScreenOnDialogTitle", + "settingsDoubleBackExit", + "settingsConfirmationTile", + "settingsConfirmationDialogTitle", + "settingsConfirmationBeforeDeleteItems", + "settingsConfirmationBeforeMoveToBinItems", + "settingsConfirmationBeforeMoveUndatedItems", + "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", + "settingsNavigationDrawerTile", + "settingsNavigationDrawerEditorPageTitle", + "settingsNavigationDrawerBanner", + "settingsNavigationDrawerTabTypes", + "settingsNavigationDrawerTabAlbums", + "settingsNavigationDrawerTabPages", + "settingsNavigationDrawerAddAlbum", + "settingsThumbnailSectionTitle", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayPageTitle", + "settingsThumbnailShowFavouriteIcon", + "settingsThumbnailShowTagIcon", + "settingsThumbnailShowLocationIcon", + "settingsThumbnailShowMotionPhotoIcon", + "settingsThumbnailShowRating", + "settingsThumbnailShowRawIcon", + "settingsThumbnailShowVideoDuration", + "settingsCollectionQuickActionsTile", + "settingsCollectionQuickActionEditorPageTitle", + "settingsCollectionQuickActionTabBrowsing", + "settingsCollectionQuickActionTabSelecting", + "settingsCollectionBrowsingQuickActionEditorBanner", + "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", + "settingsViewerSectionTitle", + "settingsViewerGestureSideTapNext", + "settingsViewerUseCutout", + "settingsViewerMaximumBrightness", + "settingsMotionPhotoAutoPlay", + "settingsImageBackground", + "settingsViewerQuickActionsTile", + "settingsViewerQuickActionEditorPageTitle", + "settingsViewerQuickActionEditorBanner", + "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle", + "settingsViewerQuickActionEditorAvailableButtonsSectionTitle", + "settingsViewerQuickActionEmpty", + "settingsViewerOverlayTile", + "settingsViewerOverlayPageTitle", + "settingsViewerShowOverlayOnOpening", + "settingsViewerShowMinimap", + "settingsViewerShowInformation", + "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", + "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", + "settingsViewerShowOverlayThumbnails", + "settingsViewerEnableOverlayBlurEffect", + "settingsViewerSlideshowTile", + "settingsViewerSlideshowPageTitle", + "settingsSlideshowRepeat", + "settingsSlideshowShuffle", + "settingsSlideshowFillScreen", + "settingsSlideshowAnimatedZoomEffect", + "settingsSlideshowTransitionTile", + "settingsSlideshowIntervalTile", + "settingsSlideshowVideoPlaybackTile", + "settingsSlideshowVideoPlaybackDialogTitle", + "settingsVideoPageTitle", + "settingsVideoSectionTitle", + "settingsVideoShowVideos", + "settingsVideoEnableHardwareAcceleration", + "settingsVideoAutoPlay", + "settingsVideoLoopModeTile", + "settingsVideoLoopModeDialogTitle", + "settingsVideoBackgroundMode", + "settingsVideoBackgroundModeDialogTitle", + "settingsSubtitleThemeTile", + "settingsSubtitleThemePageTitle", + "settingsSubtitleThemeSample", + "settingsSubtitleThemeTextAlignmentTile", + "settingsSubtitleThemeTextAlignmentDialogTitle", + "settingsSubtitleThemeTextPositionTile", + "settingsSubtitleThemeTextPositionDialogTitle", + "settingsSubtitleThemeTextSize", + "settingsSubtitleThemeShowOutline", + "settingsSubtitleThemeTextColor", + "settingsSubtitleThemeTextOpacity", + "settingsSubtitleThemeBackgroundColor", + "settingsSubtitleThemeBackgroundOpacity", + "settingsSubtitleThemeTextAlignmentLeft", + "settingsSubtitleThemeTextAlignmentCenter", + "settingsSubtitleThemeTextAlignmentRight", + "settingsVideoControlsTile", + "settingsVideoControlsPageTitle", + "settingsVideoButtonsTile", + "settingsVideoGestureDoubleTapTogglePlay", + "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsPrivacySectionTitle", + "settingsAllowInstalledAppAccess", + "settingsAllowInstalledAppAccessSubtitle", + "settingsAllowErrorReporting", + "settingsSaveSearchHistory", + "settingsEnableBin", + "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", + "settingsAllowMediaManagement", + "settingsHiddenItemsTile", + "settingsHiddenItemsPageTitle", + "settingsHiddenItemsTabFilters", + "settingsHiddenFiltersBanner", + "settingsHiddenFiltersEmpty", + "settingsHiddenItemsTabPaths", + "settingsHiddenPathsBanner", + "addPathTooltip", + "settingsStorageAccessTile", + "settingsStorageAccessPageTitle", + "settingsStorageAccessBanner", + "settingsStorageAccessEmpty", + "settingsStorageAccessRevokeTooltip", + "settingsAccessibilitySectionTitle", + "settingsRemoveAnimationsTile", + "settingsRemoveAnimationsDialogTitle", + "settingsTimeToTakeActionTile", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplaySectionTitle", + "settingsThemeBrightnessTile", + "settingsThemeBrightnessDialogTitle", + "settingsThemeColorHighlights", + "settingsThemeEnableDynamicColor", + "settingsDisplayRefreshRateModeTile", + "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", + "settingsLanguageSectionTitle", + "settingsLanguageTile", + "settingsLanguagePageTitle", + "settingsCoordinateFormatTile", + "settingsCoordinateFormatDialogTitle", + "settingsUnitSystemTile", + "settingsUnitSystemDialogTitle", + "settingsScreenSaverPageTitle", + "settingsWidgetPageTitle", + "settingsWidgetShowOutline", + "settingsWidgetOpenPage", + "settingsWidgetDisplayedItem", + "settingsCollectionTile", + "statsPageTitle", + "statsWithGps", + "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", + "statsTopPlacesSectionTitle", + "statsTopTagsSectionTitle", + "statsTopAlbumsSectionTitle", + "viewerOpenPanoramaButtonLabel", + "viewerSetWallpaperButtonLabel", + "viewerErrorUnknown", + "viewerErrorDoesNotExist", + "viewerInfoPageTitle", + "viewerInfoBackToViewerTooltip", + "viewerInfoUnknown", + "viewerInfoLabelDescription", + "viewerInfoLabelTitle", + "viewerInfoLabelDate", + "viewerInfoLabelResolution", + "viewerInfoLabelSize", + "viewerInfoLabelUri", + "viewerInfoLabelPath", + "viewerInfoLabelDuration", + "viewerInfoLabelOwner", + "viewerInfoLabelCoordinates", + "viewerInfoLabelAddress", + "mapStyleDialogTitle", + "mapStyleTooltip", + "mapZoomInTooltip", + "mapZoomOutTooltip", + "mapPointNorthUpTooltip", + "mapAttributionOsmHot", + "mapAttributionStamen", + "openMapPageTooltip", + "mapEmptyRegion", + "viewerInfoOpenEmbeddedFailureFeedback", + "viewerInfoOpenLinkText", + "viewerInfoViewXmlLinkText", + "viewerInfoSearchFieldLabel", + "viewerInfoSearchEmpty", + "viewerInfoSearchSuggestionDate", + "viewerInfoSearchSuggestionDescription", + "viewerInfoSearchSuggestionDimensions", + "viewerInfoSearchSuggestionResolution", + "viewerInfoSearchSuggestionRights", + "wallpaperUseScrollEffect", + "tagEditorPageTitle", + "tagEditorPageNewTagFieldLabel", + "tagEditorPageAddTagTooltip", + "tagEditorSectionRecent", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderState", + "tagPlaceholderPlace", + "panoramaEnableSensorControl", + "panoramaDisableSensorControl", + "sourceViewerPageTitle", + "filePickerShowHiddenFiles", + "filePickerDoNotShowHiddenFiles", + "filePickerOpenFrom", + "filePickerNoItems", + "filePickerUseThisFolder" + ], + + "hu": [ + "welcomeOptional", + "welcomeTermsToggle", + "itemCount", + "columnCount", + "timeSeconds", + "timeMinutes", + "timeDays", + "focalLength", + "showButtonLabel", + "hideButtonLabel", + "changeTooltip", + "clearTooltip", + "showTooltip", + "hideTooltip", + "actionRemove", + "resetTooltip", + "pickTooltip", + "doubleBackExitMessage", + "sourceStateCataloguing", + "sourceStateLocatingCountries", + "sourceStateLocatingPlaces", + "chipActionGoToAlbumPage", + "chipActionGoToCountryPage", + "chipActionGoToPlacePage", + "chipActionGoToTagPage", + "chipActionFilterOut", + "chipActionFilterIn", + "chipActionHide", + "chipActionLock", + "chipActionPin", + "chipActionUnpin", + "chipActionSetCover", + "chipActionShowCountryStates", + "chipActionCreateVault", + "chipActionConfigureVault", + "entryActionConvert", + "entryActionRestore", + "entryActionRotateCCW", + "entryActionRotateCW", + "entryActionFlip", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", + "entryActionViewSource", + "entryActionShowGeoTiffOnMap", + "entryActionConvertMotionPhotoToStillImage", + "entryActionViewMotionPhotoVideo", + "entryActionOpen", + "entryActionSetAs", + "entryActionOpenMap", + "entryActionRemoveFavourite", + "videoActionCaptureFrame", + "videoActionUnmute", + "videoActionPause", + "videoActionReplay10", + "videoActionSkip10", + "videoActionSelectStreams", + "videoActionSetSpeed", + "viewerActionLock", + "viewerActionUnlock", + "slideshowActionResume", + "slideshowActionShowInCollection", + "entryInfoActionEditLocation", + "entryInfoActionEditTitleDescription", + "entryInfoActionEditRating", + "entryInfoActionEditTags", + "entryInfoActionExportMetadata", + "entryInfoActionRemoveLocation", + "filterAspectRatioLandscapeLabel", + "filterAspectRatioPortraitLabel", + "filterBinLabel", + "filterFavouriteLabel", + "filterNoDateLabel", + "filterNoAddressLabel", + "filterLocatedLabel", + "filterNoLocationLabel", + "filterNoRatingLabel", + "filterTaggedLabel", + "filterNoTagLabel", + "filterRatingRejectedLabel", + "filterTypeAnimatedLabel", + "filterTypeMotionPhotoLabel", + "filterTypeRawLabel", + "filterTypeSphericalVideoLabel", + "filterTypeGeotiffLabel", + "filterMimeImageLabel", + "accessibilityAnimationsRemove", + "accessibilityAnimationsKeep", + "albumTierPinned", + "albumTierSpecial", + "albumTierApps", + "albumTierVaults", + "coordinateFormatDms", + "coordinateFormatDecimal", + "coordinateDms", + "coordinateDmsNorth", + "coordinateDmsSouth", + "coordinateDmsEast", + "coordinateDmsWest", + "displayRefreshRatePreferHighest", + "displayRefreshRatePreferLowest", + "keepScreenOnVideoPlayback", + "keepScreenOnViewerOnly", + "lengthUnitPixel", + "lengthUnitPercent", + "mapStyleGoogleNormal", + "mapStyleGoogleHybrid", + "mapStyleGoogleTerrain", + "mapStyleHuaweiNormal", + "mapStyleHuaweiTerrain", + "mapStyleOsmHot", + "mapStyleStamenToner", + "mapStyleStamenWatercolor", + "nameConflictStrategyReplace", + "nameConflictStrategySkip", + "subtitlePositionTop", + "subtitlePositionBottom", + "themeBrightnessLight", + "unitSystemMetric", + "unitSystemImperial", + "vaultLockTypePattern", + "vaultLockTypePin", + "settingsVideoEnablePip", + "videoControlsPlaySeek", + "videoControlsPlayOutside", + "videoLoopModeShortOnly", + "videoPlaybackSkip", + "videoPlaybackMuted", + "videoPlaybackWithSound", + "viewerTransitionSlide", + "viewerTransitionParallax", + "viewerTransitionFade", + "viewerTransitionZoomIn", + "wallpaperTargetHome", + "wallpaperTargetLock", + "wallpaperTargetHomeLock", + "widgetDisplayedItemRandom", + "widgetDisplayedItemMostRecent", + "widgetOpenPageHome", + "widgetOpenPageCollection", + "widgetOpenPageViewer", + "rootDirectoryDescription", + "otherDirectoryDescription", + "storageAccessDialogMessage", + "restrictedAccessDialogMessage", + "notEnoughSpaceDialogMessage", + "missingSystemFilePickerDialogMessage", + "unsupportedTypeDialogMessage", + "nameConflictDialogSingleSourceMessage", + "nameConflictDialogMultipleSourceMessage", + "addShortcutDialogLabel", + "addShortcutButtonLabel", + "noMatchingAppDialogMessage", + "binEntriesConfirmationDialogMessage", + "deleteEntriesConfirmationDialogMessage", + "moveUndatedConfirmationDialogMessage", + "moveUndatedConfirmationDialogSetDate", + "videoResumeDialogMessage", + "videoStartOverButtonLabel", + "videoResumeButtonLabel", + "setCoverDialogLatest", + "setCoverDialogAuto", + "setCoverDialogCustom", + "hideFilterConfirmationDialogMessage", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "patternDialogEnter", + "patternDialogConfirm", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "renameEntrySetPagePatternFieldLabel", + "renameEntrySetPageInsertTooltip", + "renameEntrySetPagePreviewSectionTitle", + "renameProcessorCounter", + "deleteSingleAlbumConfirmationDialogMessage", + "deleteMultiAlbumConfirmationDialogMessage", + "exportEntryDialogFormat", + "exportEntryDialogWidth", + "exportEntryDialogHeight", + "exportEntryDialogWriteMetadata", + "editEntryDialogCopyFromItem", + "editEntryDialogTargetFieldsHeader", + "editEntryDateDialogSetCustom", + "editEntryDateDialogCopyField", + "editEntryDateDialogExtractFromTitle", + "editEntryDateDialogShift", + "editEntryDateDialogSourceFileModifiedDate", + "durationDialogHours", + "durationDialogMinutes", + "durationDialogSeconds", + "editEntryLocationDialogTitle", + "editEntryLocationDialogSetCustom", + "editEntryLocationDialogChooseOnMap", + "editEntryLocationDialogLatitude", + "editEntryLocationDialogLongitude", + "locationPickerUseThisLocationButton", + "editEntryRatingDialogTitle", + "removeEntryMetadataDialogTitle", + "removeEntryMetadataDialogMore", + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage", + "videoSpeedDialogLabel", + "videoStreamSelectionDialogVideo", + "videoStreamSelectionDialogAudio", + "videoStreamSelectionDialogTrack", + "videoStreamSelectionDialogNoSelection", + "tooManyItemsErrorDialogMessage", + "menuActionConfigureView", + "menuActionSelect", + "menuActionSelectAll", + "menuActionSelectNone", + "menuActionStats", + "viewDialogSortSectionTitle", + "viewDialogGroupSectionTitle", + "viewDialogLayoutSectionTitle", + "viewDialogReverseSortOrder", + "tileLayoutMosaic", + "tileLayoutGrid", + "tileLayoutList", + "coverDialogTabApp", + "coverDialogTabColor", + "appPickDialogTitle", + "aboutLinkPolicy", + "aboutBugSaveLogInstruction", + "aboutBugCopyInfoInstruction", + "aboutBugReportInstruction", + "aboutBugReportButton", + "aboutCreditsSectionTitle", + "aboutCreditsWorldAtlas1", + "aboutCreditsWorldAtlas2", + "aboutLicensesSectionTitle", + "aboutLicensesBanner", + "aboutLicensesAndroidLibrariesSectionTitle", + "aboutLicensesFlutterPluginsSectionTitle", + "aboutLicensesFlutterPackagesSectionTitle", + "aboutLicensesDartPackagesSectionTitle", + "aboutLicensesShowAllButtonLabel", + "policyPageTitle", + "collectionPickPageTitle", + "collectionSelectPageTitle", + "collectionActionShowTitleSearch", + "collectionActionHideTitleSearch", + "collectionActionAddShortcut", + "collectionActionEmptyBin", + "collectionActionCopy", + "collectionActionMove", + "collectionActionRescan", + "collectionSearchTitlesHintText", + "collectionGroupAlbum", + "collectionGroupMonth", + "collectionGroupDay", + "collectionGroupNone", + "collectionDeleteFailureFeedback", + "collectionCopyFailureFeedback", + "collectionMoveFailureFeedback", + "collectionRenameFailureFeedback", + "collectionEditFailureFeedback", + "collectionExportFailureFeedback", + "collectionCopySuccessFeedback", + "collectionMoveSuccessFeedback", + "collectionRenameSuccessFeedback", + "collectionEditSuccessFeedback", + "collectionEmptyFavourites", + "collectionEmptyVideos", + "collectionEmptyImages", + "collectionEmptyGrantAccessButtonLabel", + "collectionSelectSectionTooltip", + "collectionDeselectSectionTooltip", + "drawerCollectionAll", + "drawerCollectionAnimated", + "drawerCollectionMotionPhotos", + "drawerCollectionRaws", + "drawerCollectionSphericalVideos", + "drawerCountryPage", + "drawerPlacePage", + "drawerTagPage", + "sortByDate", + "sortByName", + "sortByItemCount", + "sortBySize", + "sortByAlbumFileName", + "sortByRating", + "sortOrderNewestFirst", + "sortOrderOldestFirst", + "sortOrderAtoZ", + "sortOrderZtoA", + "sortOrderHighestFirst", + "sortOrderLowestFirst", + "sortOrderLargestFirst", + "sortOrderSmallestFirst", + "albumGroupTier", + "albumGroupType", + "albumGroupVolume", + "albumGroupNone", + "albumMimeTypeMixed", + "albumPickPageTitleCopy", + "albumPickPageTitleExport", + "albumPickPageTitleMove", + "albumPickPageTitlePick", + "albumScreenRecordings", + "albumVideoCaptures", + "albumEmpty", + "createAlbumButtonLabel", + "countryPageTitle", + "countryEmpty", + "statePageTitle", + "stateEmpty", + "placePageTitle", + "placeEmpty", + "tagPageTitle", + "tagEmpty", + "binPageTitle", + "searchCollectionFieldHint", + "searchRecentSectionTitle", + "searchDateSectionTitle", + "searchAlbumsSectionTitle", + "searchCountriesSectionTitle", + "searchStatesSectionTitle", + "searchPlacesSectionTitle", + "searchTagsSectionTitle", + "searchRatingSectionTitle", + "searchMetadataSectionTitle", + "settingsPageTitle", + "settingsSystemDefault", + "settingsDefault", + "settingsDisabled", + "settingsModificationWarningDialogMessage", + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsActionExport", + "settingsActionExportDialogTitle", + "settingsActionImport", + "settingsActionImportDialogTitle", + "appExportCovers", + "appExportFavourites", + "appExportSettings", + "settingsNavigationSectionTitle", + "settingsHomeTile", + "settingsHomeDialogTitle", + "settingsShowBottomNavigationBar", + "settingsKeepScreenOnTile", + "settingsKeepScreenOnDialogTitle", + "settingsDoubleBackExit", + "settingsConfirmationTile", + "settingsConfirmationDialogTitle", + "settingsConfirmationBeforeDeleteItems", + "settingsConfirmationBeforeMoveToBinItems", + "settingsConfirmationBeforeMoveUndatedItems", + "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", + "settingsNavigationDrawerTile", + "settingsNavigationDrawerEditorPageTitle", + "settingsNavigationDrawerBanner", + "settingsNavigationDrawerTabTypes", + "settingsNavigationDrawerTabAlbums", + "settingsNavigationDrawerTabPages", + "settingsNavigationDrawerAddAlbum", + "settingsThumbnailSectionTitle", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayPageTitle", + "settingsThumbnailShowFavouriteIcon", + "settingsThumbnailShowTagIcon", + "settingsThumbnailShowLocationIcon", + "settingsThumbnailShowMotionPhotoIcon", + "settingsThumbnailShowRating", + "settingsThumbnailShowRawIcon", + "settingsThumbnailShowVideoDuration", + "settingsCollectionQuickActionsTile", + "settingsCollectionQuickActionEditorPageTitle", + "settingsCollectionQuickActionTabBrowsing", + "settingsCollectionQuickActionTabSelecting", + "settingsCollectionBrowsingQuickActionEditorBanner", + "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", + "settingsViewerSectionTitle", + "settingsViewerGestureSideTapNext", + "settingsViewerUseCutout", + "settingsViewerMaximumBrightness", + "settingsMotionPhotoAutoPlay", + "settingsImageBackground", + "settingsViewerQuickActionsTile", + "settingsViewerQuickActionEditorPageTitle", + "settingsViewerQuickActionEditorBanner", + "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle", + "settingsViewerQuickActionEditorAvailableButtonsSectionTitle", + "settingsViewerQuickActionEmpty", + "settingsViewerOverlayTile", + "settingsViewerOverlayPageTitle", + "settingsViewerShowOverlayOnOpening", + "settingsViewerShowMinimap", + "settingsViewerShowInformation", + "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", + "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", + "settingsViewerShowOverlayThumbnails", + "settingsViewerEnableOverlayBlurEffect", + "settingsViewerSlideshowTile", + "settingsViewerSlideshowPageTitle", + "settingsSlideshowRepeat", + "settingsSlideshowShuffle", + "settingsSlideshowFillScreen", + "settingsSlideshowAnimatedZoomEffect", + "settingsSlideshowTransitionTile", + "settingsSlideshowIntervalTile", + "settingsSlideshowVideoPlaybackTile", + "settingsSlideshowVideoPlaybackDialogTitle", + "settingsVideoPageTitle", + "settingsVideoSectionTitle", + "settingsVideoShowVideos", + "settingsVideoEnableHardwareAcceleration", + "settingsVideoAutoPlay", + "settingsVideoLoopModeTile", + "settingsVideoLoopModeDialogTitle", + "settingsVideoBackgroundMode", + "settingsVideoBackgroundModeDialogTitle", + "settingsSubtitleThemeTile", + "settingsSubtitleThemePageTitle", + "settingsSubtitleThemeSample", + "settingsSubtitleThemeTextAlignmentTile", + "settingsSubtitleThemeTextAlignmentDialogTitle", + "settingsSubtitleThemeTextPositionTile", + "settingsSubtitleThemeTextPositionDialogTitle", + "settingsSubtitleThemeTextSize", + "settingsSubtitleThemeShowOutline", + "settingsSubtitleThemeTextColor", + "settingsSubtitleThemeTextOpacity", + "settingsSubtitleThemeBackgroundColor", + "settingsSubtitleThemeBackgroundOpacity", + "settingsSubtitleThemeTextAlignmentLeft", + "settingsSubtitleThemeTextAlignmentCenter", + "settingsSubtitleThemeTextAlignmentRight", + "settingsVideoControlsTile", + "settingsVideoControlsPageTitle", + "settingsVideoButtonsTile", + "settingsVideoGestureDoubleTapTogglePlay", + "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsPrivacySectionTitle", + "settingsAllowInstalledAppAccess", + "settingsAllowInstalledAppAccessSubtitle", + "settingsAllowErrorReporting", + "settingsSaveSearchHistory", + "settingsEnableBin", + "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", + "settingsAllowMediaManagement", + "settingsHiddenItemsTile", + "settingsHiddenItemsPageTitle", + "settingsHiddenItemsTabFilters", + "settingsHiddenFiltersBanner", + "settingsHiddenFiltersEmpty", + "settingsHiddenItemsTabPaths", + "settingsHiddenPathsBanner", + "addPathTooltip", + "settingsStorageAccessTile", + "settingsStorageAccessPageTitle", + "settingsStorageAccessBanner", + "settingsStorageAccessEmpty", + "settingsStorageAccessRevokeTooltip", + "settingsAccessibilitySectionTitle", + "settingsRemoveAnimationsTile", + "settingsRemoveAnimationsDialogTitle", + "settingsTimeToTakeActionTile", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplaySectionTitle", + "settingsThemeBrightnessTile", + "settingsThemeBrightnessDialogTitle", + "settingsThemeColorHighlights", + "settingsThemeEnableDynamicColor", + "settingsDisplayRefreshRateModeTile", + "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", + "settingsLanguageSectionTitle", + "settingsLanguageTile", + "settingsLanguagePageTitle", + "settingsCoordinateFormatTile", + "settingsCoordinateFormatDialogTitle", + "settingsUnitSystemTile", + "settingsUnitSystemDialogTitle", + "settingsScreenSaverPageTitle", + "settingsWidgetPageTitle", + "settingsWidgetShowOutline", + "settingsWidgetOpenPage", + "settingsWidgetDisplayedItem", + "settingsCollectionTile", + "statsPageTitle", + "statsWithGps", + "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", + "statsTopPlacesSectionTitle", + "statsTopTagsSectionTitle", + "statsTopAlbumsSectionTitle", + "viewerOpenPanoramaButtonLabel", + "viewerSetWallpaperButtonLabel", + "viewerErrorUnknown", + "viewerErrorDoesNotExist", + "viewerInfoPageTitle", + "viewerInfoBackToViewerTooltip", + "viewerInfoUnknown", + "viewerInfoLabelDescription", + "viewerInfoLabelTitle", + "viewerInfoLabelDate", + "viewerInfoLabelResolution", + "viewerInfoLabelSize", + "viewerInfoLabelUri", + "viewerInfoLabelPath", + "viewerInfoLabelDuration", + "viewerInfoLabelOwner", + "viewerInfoLabelCoordinates", + "viewerInfoLabelAddress", + "mapStyleDialogTitle", + "mapStyleTooltip", + "mapZoomInTooltip", + "mapZoomOutTooltip", + "mapPointNorthUpTooltip", + "mapAttributionOsmHot", + "mapAttributionStamen", + "openMapPageTooltip", + "mapEmptyRegion", + "viewerInfoOpenEmbeddedFailureFeedback", + "viewerInfoOpenLinkText", + "viewerInfoViewXmlLinkText", + "viewerInfoSearchFieldLabel", + "viewerInfoSearchEmpty", + "viewerInfoSearchSuggestionDate", + "viewerInfoSearchSuggestionDescription", + "viewerInfoSearchSuggestionDimensions", + "viewerInfoSearchSuggestionResolution", + "viewerInfoSearchSuggestionRights", + "wallpaperUseScrollEffect", + "tagEditorPageTitle", + "tagEditorPageNewTagFieldLabel", + "tagEditorPageAddTagTooltip", + "tagEditorSectionRecent", + "tagEditorSectionPlaceholders", + "tagPlaceholderCountry", + "tagPlaceholderState", "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", @@ -2846,64 +4022,29 @@ ], "it": [ - "chipActionGoToPlacePage", - "lengthUnitPixel", - "lengthUnitPercent", - "vaultLockTypePattern", - "settingsVideoEnablePip", - "patternDialogEnter", - "patternDialogConfirm", - "exportEntryDialogWriteMetadata", - "drawerPlacePage", - "placePageTitle", - "placeEmpty", - "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle" + "settingsCollectionBurstPatternsTile" ], "ja": [ "columnCount", - "chipActionGoToPlacePage", - "chipActionFilterIn", - "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", - "filterAspectRatioLandscapeLabel", - "filterAspectRatioPortraitLabel", - "filterNoAddressLabel", + "viewerActionLock", + "viewerActionUnlock", "filterLocatedLabel", "filterTaggedLabel", "albumTierVaults", - "keepScreenOnVideoPlayback", - "lengthUnitPixel", - "lengthUnitPercent", - "subtitlePositionTop", "subtitlePositionBottom", - "vaultLockTypePattern", - "vaultLockTypePin", - "vaultLockTypePassword", - "settingsVideoEnablePip", - "newVaultWarningDialogMessage", - "newVaultDialogTitle", - "configureVaultDialogTitle", - "vaultDialogLockModeWhenScreenOff", - "vaultDialogLockTypeLabel", - "patternDialogEnter", - "patternDialogConfirm", - "pinDialogEnter", - "pinDialogConfirm", - "passwordDialogEnter", - "passwordDialogConfirm", - "authenticateToConfigureVault", - "authenticateToUnlockVault", "vaultBinUsageDialogMessage", "exportEntryDialogWriteMetadata", - "tooManyItemsErrorDialogMessage", - "drawerPlacePage", - "placePageTitle", + "stateEmpty", "placeEmpty", + "searchStatesSectionTitle", "settingsModificationWarningDialogMessage", "settingsConfirmationVaultDataLoss", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerShowDescription", "settingsVideoBackgroundMode", "settingsVideoBackgroundModeDialogTitle", @@ -2911,15 +4052,19 @@ "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface", - "settingsWidgetDisplayedItem" + "settingsWidgetDisplayedItem", + "statsTopStatesSectionTitle" ], "lt": [ "columnCount", "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", + "viewerActionLock", + "viewerActionUnlock", "filterLocatedLabel", "filterTaggedLabel", "albumTierVaults", @@ -2947,37 +4092,56 @@ "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", + "searchStatesSectionTitle", "settingsModificationWarningDialogMessage", "settingsConfirmationVaultDataLoss", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerShowDescription", "settingsVideoBackgroundMode", "settingsVideoBackgroundModeDialogTitle", "settingsVideoGestureVerticalDragBrightnessVolume", "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", - "settingsDisplayUseTvInterface" + "settingsDisplayUseTvInterface", + "statsTopStatesSectionTitle", + "tagPlaceholderState" ], "nb": [ + "chipActionShowCountryStates", + "viewerActionLock", + "viewerActionUnlock", "vaultLockTypePattern", "settingsVideoEnablePip", "patternDialogEnter", "patternDialogConfirm", + "statePageTitle", + "stateEmpty", + "searchStatesSectionTitle", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle" + "settingsVideoBackgroundModeDialogTitle", + "statsTopStatesSectionTitle", + "tagPlaceholderState" ], "nl": [ "columnCount", "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", "entryActionShareImageOnly", "entryActionShareVideoOnly", - "entryInfoActionExportMetadata", + "viewerActionLock", + "viewerActionUnlock", "entryInfoActionRemoveLocation", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", @@ -2987,11 +4151,9 @@ "albumTierVaults", "keepScreenOnVideoPlayback", "lengthUnitPixel", - "lengthUnitPercent", "subtitlePositionTop", "subtitlePositionBottom", "vaultLockTypePattern", - "vaultLockTypePin", "vaultLockTypePassword", "settingsVideoEnablePip", "widgetDisplayedItemRandom", @@ -3013,10 +4175,15 @@ "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", + "searchStatesSectionTitle", "settingsModificationWarningDialogMessage", "settingsConfirmationVaultDataLoss", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerShowRatingTags", "settingsViewerShowDescription", "settingsVideoBackgroundMode", @@ -3027,7 +4194,9 @@ "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface", - "settingsWidgetDisplayedItem" + "settingsWidgetDisplayedItem", + "statsTopStatesSectionTitle", + "tagPlaceholderState" ], "nn": [ @@ -3035,8 +4204,11 @@ "sourceStateCataloguing", "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", + "viewerActionLock", + "viewerActionUnlock", "entryInfoActionRemoveLocation", "filterLocatedLabel", "filterNoLocationLabel", @@ -3176,6 +4348,8 @@ "newFilterBanner", "countryPageTitle", "countryEmpty", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", "tagPageTitle", @@ -3186,6 +4360,7 @@ "searchDateSectionTitle", "searchAlbumsSectionTitle", "searchCountriesSectionTitle", + "searchStatesSectionTitle", "searchPlacesSectionTitle", "searchTagsSectionTitle", "searchRatingSectionTitle", @@ -3241,6 +4416,8 @@ "settingsCollectionQuickActionTabSelecting", "settingsCollectionBrowsingQuickActionEditorBanner", "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerSectionTitle", "settingsViewerGestureSideTapNext", "settingsViewerUseCutout", @@ -3340,6 +4517,7 @@ "settingsDisplayUseTvInterface", "settingsWidgetOpenPage", "statsWithGps", + "statsTopStatesSectionTitle", "viewerInfoPageTitle", "viewerInfoLabelDescription", "mapPointNorthUpTooltip", @@ -3347,45 +4525,27 @@ "mapAttributionStamen", "mapEmptyRegion", "viewerInfoSearchSuggestionDimensions", - "wallpaperUseScrollEffect" - ], - - "pt": [ - "settingsVideoBackgroundModeDialogTitle" - ], - - "ro": [ - "chipActionGoToPlacePage", - "lengthUnitPixel", - "lengthUnitPercent", - "vaultLockTypePattern", - "settingsVideoEnablePip", - "patternDialogEnter", - "patternDialogConfirm", - "exportEntryDialogWriteMetadata", - "drawerPlacePage", - "placePageTitle", - "placeEmpty", - "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle" + "wallpaperUseScrollEffect", + "tagPlaceholderState" ], "ru": [ - "chipActionLock", - "vaultLockTypePattern", - "settingsVideoEnablePip", - "patternDialogEnter", - "patternDialogConfirm", + "chipActionShowCountryStates", + "viewerActionLock", + "viewerActionUnlock", "authenticateToConfigureVault", "authenticateToUnlockVault", "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", - "drawerPlacePage", - "placeEmpty", + "statePageTitle", + "stateEmpty", + "searchStatesSectionTitle", "settingsConfirmationVaultDataLoss", - "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle", - "settingsVideoGestureVerticalDragBrightnessVolume" + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", + "settingsVideoGestureVerticalDragBrightnessVolume", + "statsTopStatesSectionTitle", + "tagPlaceholderState" ], "sk": [ @@ -3394,8 +4554,11 @@ "timeSeconds", "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", + "viewerActionLock", + "viewerActionUnlock", "filterLocatedLabel", "filterNoLocationLabel", "albumTierVaults", @@ -3573,6 +4736,8 @@ "newFilterBanner", "countryPageTitle", "countryEmpty", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", "tagPageTitle", @@ -3583,6 +4748,7 @@ "searchDateSectionTitle", "searchAlbumsSectionTitle", "searchCountriesSectionTitle", + "searchStatesSectionTitle", "searchPlacesSectionTitle", "searchTagsSectionTitle", "searchRatingSectionTitle", @@ -3638,6 +4804,8 @@ "settingsCollectionQuickActionTabSelecting", "settingsCollectionBrowsingQuickActionEditorBanner", "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerSectionTitle", "settingsViewerGestureSideTapNext", "settingsViewerUseCutout", @@ -3753,6 +4921,7 @@ "statsPageTitle", "statsWithGps", "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", "statsTopPlacesSectionTitle", "statsTopTagsSectionTitle", "statsTopAlbumsSectionTitle", @@ -3800,6 +4969,7 @@ "tagEditorSectionRecent", "tagEditorSectionPlaceholders", "tagPlaceholderCountry", + "tagPlaceholderState", "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", @@ -3821,8 +4991,11 @@ "applyButtonLabel", "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", + "viewerActionLock", + "viewerActionUnlock", "albumTierVaults", "lengthUnitPixel", "lengthUnitPercent", @@ -3931,6 +5104,8 @@ "newFilterBanner", "countryPageTitle", "countryEmpty", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", "tagPageTitle", @@ -3941,6 +5116,7 @@ "searchDateSectionTitle", "searchAlbumsSectionTitle", "searchCountriesSectionTitle", + "searchStatesSectionTitle", "searchPlacesSectionTitle", "searchTagsSectionTitle", "searchRatingSectionTitle", @@ -3996,6 +5172,8 @@ "settingsCollectionQuickActionTabSelecting", "settingsCollectionBrowsingQuickActionEditorBanner", "settingsCollectionSelectionQuickActionEditorBanner", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerSectionTitle", "settingsViewerGestureSideTapNext", "settingsViewerUseCutout", @@ -4111,6 +5289,7 @@ "statsPageTitle", "statsWithGps", "statsTopCountriesSectionTitle", + "statsTopStatesSectionTitle", "statsTopPlacesSectionTitle", "statsTopTagsSectionTitle", "statsTopAlbumsSectionTitle", @@ -4158,6 +5337,7 @@ "tagEditorSectionRecent", "tagEditorSectionPlaceholders", "tagPlaceholderCountry", + "tagPlaceholderState", "tagPlaceholderPlace", "panoramaEnableSensorControl", "panoramaDisableSensorControl", @@ -4172,8 +5352,11 @@ "tr": [ "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", + "viewerActionLock", + "viewerActionUnlock", "albumTierVaults", "lengthUnitPixel", "lengthUnitPercent", @@ -4197,19 +5380,25 @@ "vaultBinUsageDialogMessage", "exportEntryDialogWriteMetadata", "drawerPlacePage", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", + "searchStatesSectionTitle", "settingsConfirmationVaultDataLoss", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsVideoBackgroundMode", "settingsVideoBackgroundModeDialogTitle", - "settingsDisablingBinWarningDialogMessage" + "settingsDisablingBinWarningDialogMessage", + "statsTopStatesSectionTitle", + "tagPlaceholderState" ], "zh": [ "chipActionGoToPlacePage", - "chipActionLock", - "chipActionCreateVault", - "chipActionConfigureVault", + "viewerActionLock", + "viewerActionUnlock", "filterLocatedLabel", "filterTaggedLabel", "albumTierVaults", @@ -4236,25 +5425,35 @@ "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", + "searchStatesSectionTitle", "settingsModificationWarningDialogMessage", "settingsConfirmationVaultDataLoss", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerShowDescription", "settingsVideoBackgroundMode", "settingsVideoBackgroundModeDialogTitle", "settingsVideoGestureVerticalDragBrightnessVolume", "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", - "settingsDisplayUseTvInterface" + "settingsDisplayUseTvInterface", + "statsTopStatesSectionTitle", + "tagPlaceholderState" ], "zh_Hant": [ "columnCount", "chipActionGoToPlacePage", "chipActionLock", + "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", + "viewerActionLock", + "viewerActionUnlock", "filterLocatedLabel", "filterTaggedLabel", "albumTierVaults", @@ -4281,16 +5480,23 @@ "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", + "statePageTitle", + "stateEmpty", "placePageTitle", "placeEmpty", + "searchStatesSectionTitle", "settingsModificationWarningDialogMessage", "settingsConfirmationVaultDataLoss", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", "settingsViewerShowDescription", "settingsVideoBackgroundMode", "settingsVideoBackgroundModeDialogTitle", "settingsVideoGestureVerticalDragBrightnessVolume", "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", - "settingsDisplayUseTvInterface" + "settingsDisplayUseTvInterface", + "statsTopStatesSectionTitle", + "tagPlaceholderState" ] } diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 3ee89824b..596f60dac 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,5 +1,5 @@ -In v1.8.4: -- view items in full-screen when selecting them -- watch videos using picture-in-picture -- navigate with TalkBack +In v1.8.5: +- navigate states for some countries (requires rescan) +- group Samsung and Sony bursts +- lock viewer when watching videos Full changelog available on GitHub \ No newline at end of file