[
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 keluokeda\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# hs_tracker\n## Android14 use this https://github.com/keluokeda/Hs\nAn automatic Hearthstone tracker for Android\n#### 炉石传说记牌器，支持Android12、Android13\n![Screenshot_20220423-132616_Hearthstone](https://user-images.githubusercontent.com/16809185/199713461-c0a16e10-e225-4c2a-894d-a1f4dfc51824.jpg)\n\n\n![image](https://user-images.githubusercontent.com/16809185/201462989-9a826302-b1a7-4674-825c-bfd13711efc9.png)\n\n\n### QQ群：825215274\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build"
  },
  {
    "path": "app/build.gradle",
    "content": "plugins {\n    id 'com.android.application'\n    id 'org.jetbrains.kotlin.android'\n    id 'kotlin-kapt'\n    id 'com.google.dagger.hilt.android'\n    id 'kotlin-parcelize'\n}\nkapt {\n    correctErrorTypes true\n}\nandroid {\n    compileSdk libs.versions.compilesdk.get().toInteger()\n\n    defaultConfig {\n        applicationId \"com.ke.hs_tracker.app\"\n        minSdk libs.versions.minsdk.get().toInteger()\n        targetSdk libs.versions.targetsdk.get().toInteger()\n        versionCode 31\n        versionName \"1.3.1\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n\n\n    }\n    buildFeatures {\n        viewBinding = true\n    }\n\n    signingConfigs {\n        myConfig {\n            storeFile file(RELEASE_FILE)\n            storePassword RELEASE_storePassword\n            keyAlias RELEASE_keyAlias\n            keyPassword RELEASE_keyPassword\n        }\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n\n        debug {\n            minifyEnabled false\n            zipAlignEnabled true\n            signingConfig signingConfigs.myConfig\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n            //signingConfig signingConfigs.config\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_17\n        targetCompatibility JavaVersion.VERSION_17\n    }\n\n    namespace 'com.ke.hs_tracker.app'\n\n\n}\n\ndependencies {\n\n\n    implementation project(path: ':module')\n\n\n    implementation(libs.core.ktx)\n    implementation(libs.appcompat)\n    implementation(libs.material)\n    implementation(libs.constraint.layout)\n    implementation(libs.fragment.ktx)\n    implementation(libs.activity)\n    implementation(libs.lifecycle.viewmodel.ktx)\n    implementation(libs.lifecycle.livedata.ktx)\n    implementation(libs.lifecycle.runtime.ktx)\n    implementation(libs.support.v4)\n    implementation(libs.logger)\n\n\n    implementation(libs.hilt.android)\n    kapt(libs.hilt.compiler)\n\n    implementation(libs.ke.mvvm)\n\n    implementation(libs.moshi)\n    kapt(libs.moshi.codegen)\n\n    implementation(libs.hilt.android)\n    kapt(libs.hilt.compiler)\n}"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "app/release/output-metadata.json",
    "content": "{\n  \"version\": 3,\n  \"artifactType\": {\n    \"type\": \"APK\",\n    \"kind\": \"Directory\"\n  },\n  \"applicationId\": \"com.ke.hs_tracker.app\",\n  \"variantName\": \"release\",\n  \"elements\": [\n    {\n      \"type\": \"SINGLE\",\n      \"filters\": [],\n      \"attributes\": [],\n      \"versionCode\": 31,\n      \"versionName\": \"1.3.1\",\n      \"outputFile\": \"app-release.apk\"\n    }\n  ],\n  \"elementType\": \"File\"\n}"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n\n    <application android:name=\".App\" />\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/ke/hs_tracker/app/App.kt",
    "content": "package com.ke.hs_tracker.app\n\nimport com.ke.hs_tracker.module.MainApplication\nimport dagger.hilt.android.HiltAndroidApp\n\n@HiltAndroidApp\nclass App : MainApplication()"
  },
  {
    "path": "app/src/main/java/com/ke/hs_tracker/app/MainActivity.kt",
    "content": "package com.ke.hs_tracker.app\n//\n//import android.os.Bundle\n//import androidx.appcompat.app.AppCompatActivity\n//import androidx.documentfile.provider.DocumentFile\n//import androidx.lifecycle.lifecycleScope\n//import com.ke.hs_tracker.app.databinding.ActivityMainBinding\n//import com.ke.hs_tracker.module.findHSDataFilesDir\n//import com.ke.hs_tracker.module.log\n//import kotlinx.coroutines.Dispatchers\n//import kotlinx.coroutines.launch\n//import kotlinx.coroutines.withContext\n//\n//class MainActivity : AppCompatActivity() {\n//\n//\n//    private lateinit var powerLogFile: DocumentFile\n//\n//    private lateinit var binding: ActivityMainBinding\n//\n////    private lateinit var inputStream: InputStream\n//\n//    private var oldLogSize = 0L\n//\n//    override fun onCreate(savedInstanceState: Bundle?) {\n//        super.onCreate(savedInstanceState)\n//        binding = ActivityMainBinding.inflate(layoutInflater)\n//        setContentView(binding.root)\n//\n//\n//\n//        binding.init.setOnClickListener {\n//            lifecycleScope.launch {\n//\n//                oldLogSize = 0\n//                val logsDir = findHSDataFilesDir(\"Logs\")\n//\n//                val file = logsDir?.findFile(\"Power.log\")\n//                if (file != null) {\n//                    \"初始化成功 $file\".log()\n//                    powerLogFile = file\n//                } else {\n//                    \"初始化失败\".log()\n//                }\n//\n//\n//            }\n//        }\n//\n//        binding.delete.setOnClickListener {\n//            try {\n//                \"删除本地日志结果 ${deleteFile(\"power.log\")}\".log()\n//\n//                val result = powerLogFile.delete()\n//                \"删除文件结果 $result\".log()\n//            } catch (e: Exception) {\n////                binding.content.text = e.message\n//                \"删除文件失败\".log()\n//                e.printStackTrace()\n//            }\n//        }\n//\n//        binding.load.setOnClickListener {\n//\n//            lifecycleScope.launch {\n//                try {\n//\n//\n//                    contentResolver.openInputStream(powerLogFile.uri)!!.reader()\n//                        .apply {\n//                            if (oldLogSize > 0) {\n//                                val skip = skip(oldLogSize)\n//                                \"跳过的字节数量 $skip\".log()\n//                            }\n//                            binding.content.text = readText().also {\n//                                oldLogSize += it.length\n//                                writeToLocal(it)\n//                            }\n//\n//                            close()\n//                        }\n//\n//                } catch (e: Exception) {\n//                    binding.content.text = e.message\n//                }\n//            }\n//\n//\n//        }\n//    }\n//\n//    private suspend fun writeToLocal(content: String) {\n//        withContext(Dispatchers.IO) {\n//            openFileOutput(\"power.log\", MODE_APPEND)\n//                .writer().apply {\n//                    append(content)\n//                    flush()\n//                    close()\n//                }\n//        }\n//    }\n//\n//\n////    private suspend fun getPowerLogFileSize(): Long {\n////        return withContext(Dispatchers.IO) {\n////            try {\n////                contentResolver.query(powerLogFile.uri, arrayOf(OpenableColumns.SIZE), null, null)\n////                    ?.apply {\n////                        moveToFirst()\n////                        return@withContext getLong(0).also {\n////                            close()\n////                        }\n////\n////                    }\n////            } catch (e: Exception) {\n////                e.printStackTrace()\n////                return@withContext 0\n////            }\n////\n////\n////\n////            0\n////        }\n////    }\n//}"
  },
  {
    "path": "build.gradle",
    "content": "\n\nplugins {\n    id 'com.android.application' version '8.0.2' apply false\n    id 'com.android.library' version '8.0.2' apply false\n    id 'org.jetbrains.kotlin.android' version '1.8.10' apply false\n    id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false\n    id 'com.google.dagger.hilt.android' version '2.45' apply false\n\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}"
  },
  {
    "path": "core/.gitignore",
    "content": "/build"
  },
  {
    "path": "core/build.gradle",
    "content": "plugins {\n    id 'java-library'\n    id 'org.jetbrains.kotlin.jvm'\n    id 'kotlin-kapt'\n}\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_1_8\n    targetCompatibility = JavaVersion.VERSION_1_8\n}\n\ndependencies {\n\n    implementation(libs.retrofit)\n    implementation(libs.retrofit.converter.moshi)\n    implementation(libs.moshi)\n    kapt(libs.moshi.codegen)\n    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'\n//    implementation(\"com.squareup.moshi:moshi-kotlin:1.13.0\")\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/api/HearthStoneJsonApi.kt",
    "content": "package com.ke.hs_tracker.core.api\n\nimport com.ke.hs_tracker.core.entity.Card\nimport retrofit2.http.GET\nimport retrofit2.http.Path\n\ninterface HearthStoneJsonApi {\n\n\n    /**\n     * 获取卡牌数据\n     */\n    @GET(\"v1/{versionCode}/{region}/cards.json\")\n    suspend fun getCardJsonList(\n        @Path(\"versionCode\") versionCode: String,\n        @Path(\"region\") region: String,\n    ): List<Card>\n\n\n    companion object {\n        const val BASE_URL = \"https://api.hearthstonejson.com/\"\n    }\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/BlockType.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\nenum class BlockType {\n    /**\n     * 攻击\n     */\n    Attack,\n\n    /**\n     * 死亡\n     */\n    Deaths,\n\n    /**\n     * 触发\n     */\n    Trigger,\n\n    /**\n     * 打出一张卡牌\n     */\n    Play,\n\n    /**\n     * 卡牌生效\n     */\n    Power,\n\n    /**\n     * 交易\n     */\n    Trade\n\n}\n\ninternal fun String.toBlockType(fallback: BlockType = BlockType.Trigger): BlockType {\n\n    BlockType.values().forEach {\n        if (it.name.equals(this, true)) {\n            return it\n        }\n    }\n\n    return fallback\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/Card.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\nimport com.squareup.moshi.FromJson\nimport com.squareup.moshi.JsonClass\nimport com.squareup.moshi.ToJson\n\n@JsonClass(generateAdapter = true)\ndata class Card(\n    val name: String,\n    val cost: Int = 0,\n    val id: String,\n    val dbfId: Int,\n    val text: String = \"\",\n    //属于哪个版本 例如 TGT\n    val set: String,\n    val type: CardType = CardType.None,\n    val cardClass: CardClass,\n    val classes: List<CardClass> = emptyList(),\n    val flavor: String,\n    val attach: Int = 0,\n    val health: Int = 0\n)\n\nenum class CardType {\n\n\n    /**\n     * 英雄\n     */\n    Hero,\n\n    /**\n     * 英雄技能\n     */\n    HeroPower,\n\n\n    /**\n     *衍生牌\n     */\n    Enchantment,\n\n    /**\n     * 法术\n     */\n    Spell,\n\n    /**\n     * 随从\n     */\n    Minion,\n\n    /**\n     * 武器\n     */\n    Weapon,\n\n\n    None\n}\n\nclass CardTypeAdapter {\n\n    @FromJson\n    fun fromJson(value: String): CardType {\n\n        return CardType.values()\n            .find { it.name.equals(value.replace(\"_\", \"\"), true) } ?: CardType.None\n\n    }\n\n    @ToJson\n    fun toJson(cardType: CardType) = cardType.name.uppercase()\n}\n"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/CardClass.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\nenum class CardClass {\n    /**\n     * 法师\n     */\n    Mage,\n\n    /**\n     * 术士\n     */\n    Warlock,\n\n    /**\n     * 牧师\n     */\n    Priest,\n\n    /**\n     * 德鲁伊\n     */\n    Druid,\n\n\n    Neutral\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/CurrentDeck.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\ndata class CurrentDeck(\n    val name: String,\n    val code: String\n)\n"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/Entity.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\nimport com.ke.hs_tracker.core.parser.PowerParserImpl\n\n\n//有三种形式\n//1,Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1]\n//2,Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1]\n//3,Entity=失落的裤子#5629\ndata class Entity(\n    val entityName: String,\n    val gameCardType: GameCardType? = null,\n    val id: Int = -1,\n    val zone: Zone = Zone.Play,\n    val zonePosition: Int = -1,\n    val cardId: String? = null,\n    val player: Int = -1\n) {\n\n    val entityType: EntityType\n        get() = when {\n            gameCardType == null && id == -1 && zone == Zone.Play && zonePosition == -1 && cardId == null && player == -1 -> EntityType.Name\n            gameCardType == GameCardType.Invalid -> EntityType.Invalid\n            else -> EntityType.Clear\n        }\n\n\n    companion object {\n        internal fun createFromContent(content: String): Entity? {\n\n            if (content == \"0\") {\n                return null\n            }\n\n\n            var matchResult = PowerParserImpl.FULL_ENTITY_CONTENT1_PATTERN.matchEntire(content)\n            if (matchResult != null) {\n                return Entity(\n                    matchResult.groupValues[1],\n                    matchResult.groupValues[2].toCardType(GameCardType.Invalid),\n                    matchResult.groupValues[3].toIntOrNull() ?: 0,\n                    matchResult.groupValues[4].toZone(),\n                    matchResult.groupValues[5].toIntOrNull() ?: 0,\n                    matchResult.groupValues[6].ifBlank { null },\n                    matchResult.groupValues[7].toIntOrNull() ?: 0\n                )\n\n            }\n            matchResult = PowerParserImpl.FULL_ENTITY_CONTENT2_PATTERN.matchEntire(content)\n                ?: return Entity(content)\n\n            return Entity(\n                matchResult.groupValues[1],\n                GameCardType.Invalid,\n                matchResult.groupValues[2].toIntOrNull() ?: 0,\n                matchResult.groupValues[3].toZone(),\n                matchResult.groupValues[4].toIntOrNull() ?: 0,\n                matchResult.groupValues[5].ifBlank { null },\n                matchResult.groupValues[6].toIntOrNull() ?: 0\n            )\n\n        }\n    }\n}\n\nenum class EntityType {\n\n    //Entity=失落的裤子#5629\n    Name,\n\n    //Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1]\n    Invalid,\n\n    //Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1]\n    Clear\n}\n\n\ndata class GameEntity(\n    val gameCardType: GameCardType,\n    val entityId: Int\n)\n\n/**\n * 玩家\n */\ndata class Player(\n    val entityId: Int,\n    val playerId: Int,\n    val controller: Int,\n    val gameCardType: GameCardType,\n    val heroEntity: Int,\n    /**\n     * 手牌上限\n     */\n    val maxHandSize: Int,\n    /**\n     * 起始手牌\n     */\n    val startHandSize: Int,\n    val teamId: Int,\n    /**\n     * 费用上限 一般为10\n     */\n    val maxResources: Int\n) {\n\n    companion object {\n        internal fun fromMap(map: Map<String, String>): Player {\n\n            return Player(\n                entityId = map[\"entityid\"]?.toIntOrNull() ?: 0,\n                playerId = map[\"playerid\"]?.toIntOrNull() ?: 0,\n                controller = map[\"controller\"]?.toIntOrNull() ?: 0,\n                gameCardType = (map[\"cardtype\"] ?: \"\").toCardType(),\n                heroEntity = map[\"heroentity\"]?.toIntOrNull() ?: 0,\n                maxHandSize = map[\"maxhandsize\"]?.toIntOrNull() ?: 0,\n                startHandSize = map[\"starthandsize\"]?.toIntOrNull() ?: 0,\n                teamId = map[\"teamid\"]?.toIntOrNull() ?: 0,\n                maxResources = map[\"maxresources\"]?.toIntOrNull() ?: 0\n            )\n\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/GameCardType.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\n/**\n * 卡牌类型\n */\nenum class GameCardType {\n\n    Game,\n\n    /**\n     * 玩家\n     */\n    Player,\n\n    /**\n     * 英雄\n     */\n    Hero,\n\n    /**\n     * 英雄技能\n     */\n    HeroPower,\n\n\n    /**\n     * 牌库中的牌的状态\n     */\n    Invalid,\n\n    /**\n     * 随从身上的buff或战场上的buff（例如下一张法强怪法力值减少1）\n     */\n    Enchantment,\n\n    /**\n     * 法术\n     */\n    Spell,\n\n    /**\n     * 随从\n     */\n    Minion\n}\n\n/**\n * 字符串转 CardType类型\n */\ninternal fun String.toCardType(fallback: GameCardType = GameCardType.Game): GameCardType {\n    return GameCardType.values().find {\n        it.name.equals(this.replace(\"_\", \"\"), true)\n    } ?: fallback\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/InsertStackResult.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\nsealed interface InsertStackResult {\n\n\n    /**\n     * 插入成功\n     */\n    object Success : InsertStackResult\n\n    /**\n     * 不能插入，例如不在Block内的TAG_CHANGE\n     */\n    object CanNotInsert : InsertStackResult\n\n    /**\n     * 结束了\n     * @param powerTag tag\n     * @param handled 是否处理了本次log日志，例如 FULL_ENTITY - Updating 跟着一个 FULL_ENTITY - Updating的情况，handled就是true，表示已经处理了，不需要在进行处理；如果是FULL_ENTITY - Updating\n     * 跟着一个 TAG_CHANGE，就表示没有处理，需要调用这个方法的自行处理log数据\n     */\n    data class Over(val powerTag: PowerTag, val handled: Boolean) : InsertStackResult\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/LogType.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\nenum class LogType(val replace: String) {\n    PowerTaskList(\"PowerTaskList.DebugPrintPower() -\"),\n    GameState(\"GameState.DebugPrintGame() -\")\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/Mechanics.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\n/**\n * 类型\n */\nenum class Mechanics {\n\n    /**\n     * 过载\n     */\n    Overload,\n\n    /**\n     * 战吼\n     */\n    Battlecry,\n\n    /**\n     * 嘲讽\n     */\n    Taunt,\n\n    /**\n     * 突袭\n     */\n    Rush,\n\n    /**\n     * 潜行\n     */\n    Stealth,\n\n    /**\n     * 圣盾\n     */\n    DivineShield,\n\n    /**\n     * 法强\n     */\n    SpellPower,\n\n    /**\n     * 亡语\n     */\n    Deathrattle,\n\n    /**\n     * 荣誉击杀\n     */\n    hHonorableKill,\n\n    /**\n     * 法力迸发\n     */\n    SpellBurst,\n\n    /**\n     * 腐蚀\n     */\n    Corrupt,\n\n    /**\n     * 暴怒\n     */\n    Frenzy,\n\n    /**\n     * 发现\n     */\n    Discover,\n\n    /**\n     * 冻结\n     */\n    Freeze,\n\n    /**\n     * 连击\n     */\n    Combo,\n\n    /**\n     * 无法攻击\n     */\n    CantAttack,\n\n    /**\n     * 触发\n     */\n    TriggerVisual,\n\n    /**\n     * 奥秘\n     */\n    Secret,\n\n    /**\n     * 交易\n     */\n    Tradeable,\n\n    /**\n     * 激励\n     */\n    Inspire,\n\n    /**\n     * 沉默\n     */\n    Silence,\n\n    /**\n     * 风怒\n     */\n    Windfury,\n\n    /**\n     * 回响\n     */\n    Echo\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/NestedTag.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\ninternal sealed interface NestedTag {\n    object CreateGame : NestedTag\n    data class GameEntity(val id: Int) : NestedTag\n    data class Tag(val key: String, val value: String) : NestedTag {\n        fun toPair(): Pair<String, String> = key to value\n    }\n\n    data class Player(val entityId: Int, val playerId: Int) : NestedTag\n\n    //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] CardID=\n    //FULL_ENTITY - Updating [entityName=全副武装！ id=65 zone=PLAY zonePos=0 cardId=HERO_01bp player=1] CardID=HERO_01bp\n    data class FullEntity(\n        val entity: Entity\n    ) : NestedTag\n\n    data class Block(\n        val blockType: BlockType,\n        val entity: Entity,\n        val target: Entity?\n    ) : NestedTag\n\n\n    data class TagChange(\n        val entity: Entity,\n        val tag: String,\n        val value: String\n    ) : NestedTag {\n        fun convert(): PowerTag.PowerTaskList.TagChange {\n            return PowerTag.PowerTaskList.TagChange(entity, tag, value)\n        }\n    }\n\n\n    data class ShowEntity(\n        val entity: Entity,\n        val cardId: String\n    ) : NestedTag\n\n    object BlockEnd : NestedTag\n}\n\n//internal fun NestedTag.FullEntity.toUpdating(): PowerTag.PowerTaskList.FullEntity.Updating {\n//    return PowerTag.PowerTaskList.FullEntity.Updating(\n//        entityName, cardType, id, zone, zonePosition, cardId, player\n//    )\n//}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/PowerTag.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\n\nsealed interface PowerTag {\n\n\n    sealed interface PowerTaskList : PowerTag {\n\n\n        /**\n         * 创建游戏\n         */\n        //D 19:55:18.1257030 GameState.DebugPrintPower() - CREATE_GAME\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -     GameEntity EntityID=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CARDTYPE value=GAME\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ZONE value=PLAY\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ENTITY_ID value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=937 value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=SPAWN_TIME_COUNT value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=GAME_SEED value=1950487951\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -     Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CONTROLLER value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CARDTYPE value=PLAYER\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=PLAYER_ID value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=HERO_ENTITY value=64\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=MAXHANDSIZE value=10\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=STARTHANDSIZE value=4\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=TEAM_ID value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ZONE value=PLAY\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ENTITY_ID value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=MAXRESOURCES value=10\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=SPAWN_TIME_COUNT value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=AVRANK value=336\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -     Player EntityID=3 PlayerID=2 GameAccountId=[hi=144115211015832391 lo=44511141]\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CONTROLLER value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CARDTYPE value=PLAYER\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=PLAYER_ID value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=HERO_ENTITY value=66\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=MAXHANDSIZE value=10\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=STARTHANDSIZE value=4\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=TEAM_ID value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ZONE value=PLAY\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ENTITY_ID value=3\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=MAXRESOURCES value=10\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=SPAWN_TIME_COUNT value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=AVRANK value=338\n        data class CreateGame(\n            val gameEntity: GameEntity,\n            val player1: Player,\n            val player2: Player\n        ) : PowerTaskList\n\n\n        data class TagChange(\n            val entity: Entity,\n            val tag: String,\n            val value: String,\n        ) : PowerTaskList {\n\n            /**\n             * 是否是游戏完成的标志\n             */\n            val isGameComplete: Boolean =\n                entity.entityName == \"GameEntity\" && tag.equals(\"state\", true) && value.equals(\n                    \"COMPLETE\",\n                    true\n                )\n        }\n\n\n        data class FullEntity(\n            val entity: Entity,\n            val payloads: MutableMap<String, String> = mutableMapOf()\n        ) : PowerTaskList {\n            fun append(value: Pair<String, String>) {\n                payloads[value.first] = value.second\n            }\n        }\n\n        data class ShowEntity(\n            val entity: Entity,\n            val cardId: String,\n            val payloads: MutableMap<String, String> = mutableMapOf()\n        ) : PowerTaskList {\n            fun append(value: Pair<String, String>) {\n                payloads[value.first] = value.second\n            }\n        }\n\n        data class Block(\n            val type: BlockType,\n            val entity: Entity,\n            val target: Entity?,\n            val list: List<PowerTag>\n        ) : PowerTaskList\n    }\n\n    sealed interface GameState : PowerTag {\n        data class BuildNumber(val number: String) : GameState\n\n        data class GameType(val type: String) : GameState\n\n        data class FormatType(val type: String) : GameState\n\n        data class ScenarioID(val id: String) : GameState\n\n        data class PlayerMapping(val id: Int, val name: String) : GameState {\n\n            /**\n             * 是否是先手\n             */\n            val first: Boolean = id == 1\n\n            /**\n             * 是否是当前用户\n             */\n            val isUser: Boolean = name != \"UNKNOWN HUMAN PLAYER\"\n        }\n    }\n\n\n}\n\n"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/entity/Zone.kt",
    "content": "package com.ke.hs_tracker.core.entity\n\n\nenum class Zone {\n    /**\n     * 战场\n     */\n    Play,\n\n    /**\n     * 牌库\n     */\n    Deck,\n\n    /**\n     * 发现的牌的位置\n     */\n    SetAside,\n\n    /**\n     * 墓地 打出的法术牌和死亡的随从会进入\n     */\n    Graveyard,\n\n    /**\n     * 手牌\n     */\n    Hand\n}\n\ninternal fun String.toZone(fallback: Zone = Zone.Deck): Zone {\n    return Zone.values().firstOrNull {\n        it.name.equals(this, true)\n    } ?: fallback\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/extensions.kt",
    "content": "package com.ke.hs_tracker.core\n\nimport com.ke.hs_tracker.core.parser.PowerParserImpl\nimport java.util.*\n\n\nfun String.removeTime(): Triple<String, Date, String> {\n\n    val content = substring(PowerParserImpl.TIME_PREFIX_SIZE)\n    val start = substring(0, 1)\n    val hms = substring(2, 10).split(\":\")\n    val calendar = Calendar.getInstance()\n    calendar.set(\n        Calendar.HOUR_OF_DAY, hms[0].toInt()\n    )\n    calendar.set(\n        Calendar.MINUTE, hms[1].toInt()\n    )\n    calendar.set(\n        Calendar.SECOND, hms[2].toInt()\n    )\n\n    return Triple(\n        start,\n        calendar.time,\n        content\n    )\n}\n\nfun main(){\n    val text = \"I 22:23:35.6401730 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA\"\n    println(text.removeTime().toString())\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/parser/BlockTagStack.kt",
    "content": "package com.ke.hs_tracker.core.parser\n\nimport com.ke.hs_tracker.core.entity.*\nimport java.lang.Exception\nimport java.util.*\nimport kotlin.text.lowercase\nimport kotlin.text.replace\nimport kotlin.text.toInt\n\n\ninterface BlockTagStack {\n\n    /**\n     * 插入一条日志\n     */\n    fun insert(line: String): InsertStackResult\n\n\n}\n\ninternal class BlockTagStackImpl : BlockTagStack {\n\n    private val nestedTagList = LinkedList<NestedTag>()\n\n\n    override fun insert(line: String): InsertStackResult {\n\n        //CREATE_GAME\n        var matchResult = PowerParserImpl.CREATE_GAME_PATTERN.matchEntire(line)\n\n        if (matchResult != null) {\n            //游戏开始\n            nestedTagList.clear()\n            nestedTagList.add(NestedTag.CreateGame)\n            return InsertStackResult.Success\n        }\n\n        //GameEntity EntityID=1\n        matchResult = PowerParserImpl.GAME_ENTITY_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val entityId = matchResult.groupValues[1].toInt()\n\n            nestedTagList.add(NestedTag.GameEntity(entityId))\n            return InsertStackResult.Success\n        }\n        //Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]\n        matchResult = PowerParserImpl.PLAYER_ENTITY_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val entityId = matchResult.groupValues[1].toInt()\n            val playerId = matchResult.groupValues[2].toInt()\n            nestedTagList.add(NestedTag.Player(entityId, playerId))\n            return InsertStackResult.Success\n        }\n        //tag=CARDTYPE value=GAME\n        matchResult = PowerParserImpl.TAG_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val key = matchResult.groupValues[1]\n            val value = matchResult.groupValues[2]\n            nestedTagList.add(NestedTag.Tag(key, value))\n            return InsertStackResult.Success\n        }\n\n        //BLOCK_START\n        // BlockType=TRIGGER\n        // Entity=GameEntity\n        // EffectCardId=System.Collections.Generic.List`1[System.String]\n        // EffectIndex=-1\n        // Target=0\n        // SubOption=-1\n        // TriggerKeyword=TAG_NOT_SET\n        matchResult = PowerParserImpl.BLOCK_START_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val blockType = matchResult.groupValues[1].toBlockType()\n            val entity = Entity.createFromContent(matchResult.groupValues[2])!!\n            val target = Entity.createFromContent(matchResult.groupValues[5])\n            val block = NestedTag.Block(blockType, entity, target)\n            nestedTagList.add(block)\n            return InsertStackResult.Success\n        }\n\n        matchResult = PowerParserImpl.BLOCK_END_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            //块结束了\n            nestedTagList.add(NestedTag.BlockEnd)\n//            val blockStartList = nestedTagList.filterIsInstance<NestedTag.Block>()\n            val blockCount = nestedTagList.count {\n                it is NestedTag.Block\n            }\n            val blockEndCount = nestedTagList.count { it is NestedTag.BlockEnd }\n\n            if (blockCount == blockEndCount) {\n                return InsertStackResult.Over(flushBlock(nestedTagList), true)\n            } else {\n                return InsertStackResult.Success\n            }\n\n        }\n\n        //TAG_CHANGE Entity=阿克萌德#51240 tag=CURRENT_PLAYER value=1\n        //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=1\n        matchResult = PowerParserImpl.TAG_CHANGE_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            return when (val first = nestedTagList.firstOrNull()) {\n\n                is NestedTag.FullEntity -> {\n                    val powerTag = flushFullEntityWhenFirst()\n                    //处理堆栈\n                    InsertStackResult.Over(powerTag, false)\n                }\n                is NestedTag.ShowEntity -> {\n\n                    val showEntity = PowerTag.PowerTaskList.ShowEntity(\n                        first.entity,\n                        first.cardId\n                    )\n                    nestedTagList.forEach {\n                        if (it is NestedTag.TagChange) {\n                            showEntity.payloads[it.tag] = it.value\n                        }\n                    }\n                    nestedTagList.clear()\n                    InsertStackResult.Over(showEntity, false)\n                }\n                is NestedTag.Block -> {\n                    val tagChange = NestedTag.TagChange(\n                        Entity.createFromContent(matchResult.groupValues[1])!!,\n                        matchResult.groupValues[2],\n                        matchResult.groupValues[3],\n                    )\n                    nestedTagList.add(tagChange)\n                    InsertStackResult.Success\n                }\n                else -> {\n                    InsertStackResult.CanNotInsert\n                }\n            }\n        }\n\n        //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1] CardID=\n        matchResult = PowerParserImpl.FULL_ENTITY_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val first = nestedTagList.firstOrNull()\n            if (first is NestedTag.CreateGame) {\n                //create game 接 full entity\n                val createGame = createCreateGameTag()\n                val content = matchResult.groupValues[1]\n                insertFullEntity(content)\n                return InsertStackResult.Over(createGame, true)\n\n            } else if (first is NestedTag.FullEntity) {\n                //连续两个full entity\n                val result = flushFullEntityWhenFirst()\n                insertFullEntity(matchResult.groupValues[1])\n                return InsertStackResult.Over(result, true)\n            }\n            insertFullEntity(matchResult.groupValues[1])\n            return InsertStackResult.Success\n        }\n\n        //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=85 zone=SETASIDE zonePos=0 cardId= player=2] CardID=SCH_231e\n        matchResult = PowerParserImpl.SHOW_ENTITY.matchEntire(line)\n        if (matchResult != null) {\n            val first = nestedTagList.firstOrNull()\n            val fullEntity = if (first is NestedTag.FullEntity) {\n                flushFullEntityWhenFirst()\n            } else null\n            val entity = Entity.createFromContent(matchResult.groupValues[1])!!\n            val cardId = matchResult.groupValues[2]\n            nestedTagList.add(NestedTag.ShowEntity(entity, cardId))\n            return if (fullEntity == null) {\n                InsertStackResult.Success\n            } else {\n                InsertStackResult.Over(fullEntity, true)\n            }\n\n\n        }\n\n        return InsertStackResult.CanNotInsert\n    }\n\n\n    /**\n     * 如果栈中的第一个是FullEntity，就开始处理\n     */\n    private fun flushFullEntityWhenFirst(): PowerTag.PowerTaskList.FullEntity {\n\n\n        val map = mutableMapOf<String, String>()\n        val first = nestedTagList.removeFirst() as NestedTag.FullEntity\n\n\n\n        nestedTagList.map {\n            it as NestedTag.Tag\n        }.forEach {\n            map[it.key] = it.value\n        }\n\n        //清空栈\n        nestedTagList.clear()\n\n        return PowerTag.PowerTaskList.FullEntity(\n            first.entity,\n            map\n        )\n//        return when {\n//            isInsertCardToDeck(first) -> {\n//                //插入一张卡牌到牌库\n//                flushFullEntityInsertCardToDeck()\n//            }\n//            isInsertHeroToPlay(first) -> {\n//                //放置英雄牌到战场\n//                flushFullEntityInsertHeroToPlay()\n//            }\n//            isInsertHeroPowerToPlay(first) -> {\n//                //放置英雄技能到战场\n//                flushFullEntityInsertHeroPowerToPlay()\n//            }\n//            else -> throw RuntimeException(\"无法处理的 full entity $first\")\n//        }\n    }\n\n//    /**\n//     * 是否是置入英雄技能到战场\n//     */\n//    private fun isInsertHeroPowerToPlay(fullEntity: NestedTag.FullEntity): Boolean {\n//        if (fullEntity.entity.zone != Zone.Play) {\n//            return false\n//        }\n//\n//        nestedTagList.forEach {\n//            if (it is NestedTag.Tag && \"cardType\".equals(\n//                    it.key,\n//                    true\n//                ) && GameCardType.HeroPower.name.equals(\n//                    it.value.replace(\"_\", \"\"),\n//                    true\n//                )\n//            ) {\n//                return true\n//            }\n//        }\n//\n//\n//        return false\n//    }\n\n\n//    private fun flushFullEntityInsertHeroPowerToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay {\n//        var last = nestedTagList.removeLastOrNull()\n//        //移除第一个\n//        val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity\n//        val map = mutableMapOf<String, String>()\n//        while (last != null) {\n//            when (last) {\n//                is NestedTag.Tag -> {\n//                    map[last.key] = last.value\n//                }\n//                else -> {\n//                    throw RuntimeException(\"last的类型必须是Tag 但现在是 $last\")\n//                }\n//            }\n//            last = nestedTagList.removeLastOrNull()\n//        }\n//        return PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay.createFromEntityAndMap(\n//            entity,\n//            map\n//        )\n//    }\n//\n//    private fun flushFullEntityInsertHeroToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay {\n//        var last = nestedTagList.removeLastOrNull()\n//        //移除第一个\n//        val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity\n//        val map = mutableMapOf<String, String>()\n//        while (last != null) {\n//            when (last) {\n//                is NestedTag.Tag -> {\n//                    map[last.key] = last.value\n//                }\n//                else -> {\n//                    throw RuntimeException(\"last的类型必须是Tag 但现在是 $last\")\n//                }\n//            }\n//            last = nestedTagList.removeLastOrNull()\n//        }\n//        return PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay.createFromEntityAndMap(\n//            entity,\n//            map\n//        )\n//    }\n\n//    /**\n//     * 是否是置入英雄卡到战场\n//     */\n//    private fun isInsertHeroToPlay(fullEntity: NestedTag.FullEntity): Boolean {\n//        if (fullEntity.entity.zone != Zone.Play) {\n//            return false\n//        }\n//\n//        nestedTagList.forEach {\n//            if (it is NestedTag.Tag && \"cardType\".equals(\n//                    it.key,\n//                    true\n//                ) && GameCardType.Hero.name.equals(\n//                    it.value,\n//                    true\n//                )\n//            ) {\n//                return true\n//            }\n//        }\n//\n//\n//        return false\n//    }\n\n//    /**\n//     * 是否是置入英雄卡到战场\n//     */\n//    private fun isInsertCardToDeck(fullEntity: NestedTag.FullEntity): Boolean {\n//\n//        return fullEntity.entity.zone == Zone.Deck && fullEntity.entity.gameCardType == GameCardType.Invalid\n//    }\n\n//    private fun flushFullEntityInsertCardToDeck(): PowerTag.PowerTaskList.FullEntity.InsertToDeck {\n//        var last = nestedTagList.removeLastOrNull()\n//        //移除第一个\n//        val fullEntity = nestedTagList.removeFirst() as NestedTag.FullEntity\n//        val map = mutableMapOf<String, String>()\n//        while (last != null) {\n//            when (last) {\n//                is NestedTag.Tag -> {\n//                    map[last.key] = last.value\n//                }\n//                else -> {\n//                    throw RuntimeException(\"last的类型必须是Tag 但现在是 $last\")\n//                }\n//            }\n//            last = nestedTagList.removeLastOrNull()\n//        }\n//\n//        if (map.size != 3) throw RuntimeException(\"在插入卡牌到牌库的情况下，tag数量必须是3个\")\n//\n//        return PowerTag.PowerTaskList.FullEntity.InsertToDeck.createFromEntityAndMap(\n//            fullEntity.entity,\n//            map\n//        )\n//\n//    }\n\n\n    private fun insertFullEntity(content: String) {\n        val fullEntity = createFullEntityByContent(content)\n        nestedTagList.add(fullEntity)\n    }\n\n    /**\n     * 根据字符串创建FullEntity\n     */\n    //[entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1]\n    //[entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1]\n    private fun createFullEntityByContent(content: String): NestedTag.FullEntity {\n        val entity: Entity = Entity.createFromContent(content)!!\n        return NestedTag.FullEntity(entity)\n    }\n\n\n    private fun createCreateGameTag(): PowerTag.PowerTaskList.CreateGame {\n        val first = nestedTagList.removeFirstOrNull()\n\n        if (first == NestedTag.CreateGame) {\n            val keyValueMap = mutableMapOf<String, String>()\n\n            var last = nestedTagList.removeLastOrNull()\n            var player1: Player? = null\n            var player2: Player? = null\n            while (last != null) {\n\n                when (last) {\n\n                    is NestedTag.GameEntity -> {\n                        val game = GameEntity(\n                            GameCardType.Game,\n                            last.id\n                        )\n                        return PowerTag.PowerTaskList.CreateGame(\n                            game,\n                            player1!!,\n                            player2!!\n                        )\n                    }\n                    is NestedTag.Player -> {\n                        keyValueMap[\"playerid\"] = last.playerId.toString()\n                        keyValueMap[\"entityid\"] = last.entityId.toString()\n                        if (player2 == null) {\n                            player2 = Player.fromMap(keyValueMap)\n                        } else {\n                            player1 = Player.fromMap(keyValueMap)\n                        }\n                        keyValueMap.clear()\n                    }\n                    is NestedTag.Tag -> {\n                        keyValueMap[last.key.replace(\"_\", \"\").lowercase()] = last.value\n                    }\n                    else -> throw IllegalArgumentException(\"非法状态错误 $last\")\n                }\n\n                last = nestedTagList.removeLastOrNull()\n            }\n        } else {\n            throw IllegalArgumentException(\"第一个必须是 CreateGame，但现在是 $first\")\n        }\n\n        throw RuntimeException(\"无法创建CreateGame\")\n\n    }\n\n\n    private fun flushBlock(\n        source: MutableList<NestedTag>,\n    ): PowerTag.PowerTaskList.Block {\n        //有可能出现多级嵌套\n        val first = source.removeFirst() as? NestedTag.Block\n\n        if (first == null) {\n\n            throw RuntimeException(\"列表的第一个应该是Block，但现在是不是\")\n        }\n        source.removeLast()\n\n        val payloads = mutableListOf<PowerTag>()\n\n        val tempList = mutableListOf<NestedTag>()\n\n        source.forEachIndexed { index, nestedTag ->\n            when (nestedTag) {\n                is NestedTag.Block -> {\n//                    if (tempList.isEmpty()) {\n                    tempList.add(nestedTag)\n//                    } else {\n//                        val blockStartCount = tempList.count {\n//                            it is NestedTag.Block\n//                        }\n//                        if (blockStartCount != 1) {\n//                            throw IllegalArgumentException(\"错误的block数量 $blockStartCount\")\n//                        }\n//\n//                        val pairedBlockEndIndex = findPairBlockEndIndex(source, index)\n//                        if (pairedBlockEndIndex == -1) {\n//                            throw IllegalArgumentException(\"找不到配对的结束标识\")\n//                        }\n//                        val innerBlockList = source.subList(index, pairedBlockEndIndex)\n//                        tempList.add(flushBlock(innerBlockList))\n//                    }\n                }\n\n                is NestedTag.FullEntity -> {\n                    if (tempList.isNotEmpty()) {\n                        tempList.add(nestedTag)\n                    } else {\n                        payloads.add(PowerTag.PowerTaskList.FullEntity(nestedTag.entity))\n                    }\n                }\n\n                is NestedTag.ShowEntity -> {\n                    if (tempList.isNotEmpty()) {\n                        tempList.add(nestedTag)\n                    } else {\n                        payloads.add(\n                            PowerTag.PowerTaskList.ShowEntity(\n                                nestedTag.entity, nestedTag.cardId\n                            )\n                        )\n                    }\n                }\n                is NestedTag.Tag -> {\n\n\n                    if (tempList.isNotEmpty()) {\n                        tempList.add(nestedTag)\n                    } else {\n                        val last = payloads.last()\n                        if (last is PowerTag.PowerTaskList.FullEntity) {\n                            last.append(nestedTag.toPair())\n                        } else if (last is PowerTag.PowerTaskList.ShowEntity) {\n                            last.append(nestedTag.toPair())\n                        }\n                    }\n                }\n\n\n                is NestedTag.TagChange -> {\n                    if (tempList.isNotEmpty()) {\n                        tempList.add(nestedTag)\n                    } else {\n                        payloads.add(nestedTag.convert())\n                    }\n                }\n                NestedTag.BlockEnd -> {\n                    tempList.add(NestedTag.BlockEnd)\n\n                    val blockStartCount = tempList.count {\n                        it is NestedTag.Block\n                    }\n                    val blockEndCount = tempList.count {\n                        it is NestedTag.BlockEnd\n                    }\n                    if (blockStartCount == blockEndCount) {\n                        payloads.add(flushBlock(tempList))\n                        tempList.clear()\n                    }\n                }\n                else -> {\n                    throw IllegalArgumentException(\"非法的数据 $nestedTag\")\n                }\n            }\n\n        }\n        source.clear()\n\n        return PowerTag.PowerTaskList.Block(\n            first.blockType,\n            first.entity,\n            first.target,\n            payloads\n        )\n    }\n}\n\nprivate fun findPairBlockEndIndex(source: List<NestedTag>, start: Int): Int {\n    var blockStartCount = 0\n    source.subList(start, source.size).forEachIndexed { index, it ->\n        if (it is NestedTag.Block) {\n            blockStartCount++\n        } else if (it is NestedTag.BlockEnd) {\n            if (blockStartCount == 0) {\n                return index\n            }\n            blockStartCount--\n\n        }\n    }\n\n    return -1\n}"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/parser/DeckFileObserver.kt",
    "content": "package com.ke.hs_tracker.core.parser\n\nimport com.ke.hs_tracker.core.entity.CurrentDeck\nimport com.ke.hs_tracker.core.removeTime\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport java.io.InputStream\n\n/**\n * deck文件观察者\n */\nclass DeckFileObserver(\n    private val interval: Long = 2000,\n    private val deckFileInputStreamProvider: suspend () -> InputStream?,\n) {\n    private var oldLogSize = 0L\n\n\n    fun reset(){\n        oldLogSize = 0\n    }\n\n    suspend fun start(): Flow<CurrentDeck> = flow {\n        while (true) {\n            deckFileInputStreamProvider()?.reader()?.apply {\n                if (oldLogSize > 0) {\n                    skip(oldLogSize)\n                }\n                val text = readText()\n                oldLogSize += text.length\n                val lines = text.lines()\n                    .filter { it.isNotEmpty() }\n\n                listToDeck(lines)?.apply {\n                    emit(this)\n                }\n                close()\n            }\n            delay(interval)\n        }\n    }\n\n    private fun listToDeck(list: List<String>): CurrentDeck? {\n        if (list.isEmpty()) {\n            return null\n        }\n        val contentList = list.map {\n            it.removeTime().third\n        }\n        val name = contentList.findLast {\n            it.startsWith(\"###\", true)\n        } ?: return null\n\n        val target =\n            contentList.subList(contentList.indexOf(name), contentList.size).toMutableList()\n        target.removeFirst()\n        target.removeFirst()\n        val code = target.removeFirst()\n\n        return CurrentDeck(\n            name.replace(\"### \", \"\"), code\n        )\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/parser/PowerFileObserver.kt",
    "content": "package com.ke.hs_tracker.core.parser\n\nimport com.ke.hs_tracker.core.entity.CurrentDeck\nimport com.ke.hs_tracker.core.entity.PowerTag\nimport com.ke.hs_tracker.core.removeTime\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport java.io.InputStream\n\n/**\n * deck文件观察者\n */\nclass PowerFileObserver(\n    private val interval: Long = 2000,\n    private val fileInputStreamProvider: suspend () -> InputStream?,\n) {\n    private var oldLogSize = 0L\n\n    fun reset(){\n        oldLogSize = 0\n    }\n\n    suspend fun start(): Flow<List<String>> = flow {\n\n\n        while (true) {\n            fileInputStreamProvider()?.reader()?.apply {\n                if (oldLogSize > 0) {\n                    skip(oldLogSize)\n                }\n                val text = readText()\n                oldLogSize += text.length\n                val lines = text.lines()\n                    .filter { it.isNotEmpty() }\n\n                emit(lines)\n\n                close()\n            }\n            delay(interval)\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/ke/hs_tracker/core/parser/PowerParser.kt",
    "content": "package com.ke.hs_tracker.core.parser\n\nimport com.ke.hs_tracker.core.entity.*\nimport com.ke.hs_tracker.core.entity.toCardType\nimport com.ke.hs_tracker.core.removeTime\n\n\n/**\n * 日志解析\n */\ninterface PowerParser {\n    /**\n     * 解析一行日志\n     */\n    fun parse(content: String)\n\n    /**\n     * 解析结果监听\n     */\n    var powerTagListener: ((PowerTag) -> Unit)?\n}\n\nclass PowerParserImpl : PowerParser {\n\n    private val blockTagStack: BlockTagStack = BlockTagStackImpl()\n\n\n    override var powerTagListener: ((PowerTag) -> Unit)? = null\n\n\n    override fun parse(content: String) {\n\n        val pair = checkTypeAndReturnContent(content) ?: return\n\n\n\n        if (pair.first == LogType.PowerTaskList) {\n            handlePowerTaskListLog(pair.second)\n        } else {\n            handleGameStateLog(pair.second)\n        }\n\n    }\n\n\n    /**\n     * 检查日志类型并返回去掉时间和日期前缀的内容\n     */\n    private fun checkTypeAndReturnContent(content: String): Pair<LogType, String>? {\n        if (content.length < TIME_PREFIX_SIZE) {\n            return null\n        }\n        val noTimeContent = content.substring(TIME_PREFIX_SIZE)\n        if (noTimeContent.startsWith(LogType.GameState.replace)) {\n            return LogType.GameState to noTimeContent.replace(LogType.GameState.replace, \"\").trim()\n        } else if (noTimeContent.startsWith(LogType.PowerTaskList.replace)) {\n            return LogType.PowerTaskList to noTimeContent.replace(LogType.PowerTaskList.replace, \"\")\n                .trim()\n        }\n\n        return null\n    }\n\n    private fun handlePowerTaskListLog(line: String) {\n\n        val result = blockTagStack.insert(line)\n        when (result) {\n            InsertStackResult.CanNotInsert -> {\n                handleUnSupportNestedTag(line)\n            }\n            is InsertStackResult.Over -> {\n                powerTagListener?.invoke(result.powerTag)\n                if (!result.handled) {\n                    //需要自己处理\n                    handleUnSupportNestedTag(line)\n                }\n            }\n            InsertStackResult.Success -> {\n\n            }\n        }\n\n    }\n\n    private fun handleUnSupportNestedTag(line: String) {\n        var matchResult = TAG_CHANGE_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            handleTagChangeLine(\n                matchResult.groupValues[1],\n                matchResult.groupValues[2],\n                matchResult.groupValues[3]\n            )\n        }\n    }\n\n    private fun handleTagChangeLine(content: String, tag: String, value: String) {\n\n        val entity = Entity.createFromContent(content)!!\n\n        powerTagListener?.invoke(PowerTag.PowerTaskList.TagChange(entity, tag, value))\n    }\n\n    private fun handleGameStateLog(content: String) {\n\n\n        BUILD_NUMBER_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.BuildNumber(\n                groupValues[1]\n            )\n            powerTagListener?.invoke(tag)\n            return\n        }\n\n        GAME_TYPE_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.GameType(\n                groupValues[1]\n            )\n            powerTagListener?.invoke(tag)\n            return\n        }\n        FORMAT_TYPE_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.FormatType(\n                groupValues[1]\n            )\n\n            powerTagListener?.invoke(tag)\n            return\n        }\n\n        SCENARIO_ID_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.ScenarioID(\n                groupValues[1]\n            )\n            powerTagListener?.invoke(tag)\n            return\n        }\n        PLAYER_MAPPING_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.PlayerMapping(\n                groupValues[1].toInt(), groupValues[2]\n            )\n            powerTagListener?.invoke(tag)\n            return\n        }\n\n    }\n\n\n    companion object {\n        const val TIME_PREFIX_SIZE = 19\n\n        //CREATE_GAME\n        internal val CREATE_GAME_PATTERN = Regex(\"CREATE_GAME\")\n\n        //BuildNumber=127581\n        internal val BUILD_NUMBER_PATTERN = Regex(\"BuildNumber=(.*)\")\n\n        //GameType=GT_CASUAL\n        internal val GAME_TYPE_PATTERN = Regex(\"GameType=(.*)\")\n\n        //FormatType=FT_WILD\n        internal val FORMAT_TYPE_PATTERN = Regex(\"FormatType=(.*)\")\n\n        //ScenarioID=2\n        internal val SCENARIO_ID_PATTERN = Regex(\"ScenarioID=(.*)\")\n\n        //PlayerID=2, PlayerName=阿克萌德#51240\n        internal val PLAYER_MAPPING_PATTERN = Regex(\"PlayerID=(.*), PlayerName=(.*)\")\n\n        // tag=CARDTYPE value=GAME\n        internal val TAG_PATTERN = Regex(\"tag=(.*) value=(.*)\")\n\n        //TAG_CHANGE Entity=GameEntity tag=STATE value=RUNNING\n        internal val TAG_CHANGE_PATTERN = Regex(\"TAG_CHANGE Entity=(.*) tag=(.*) value=(.*)\")\n\n\n        //GameEntity EntityID=1\n        internal val GAME_ENTITY_PATTERN = Regex(\"GameEntity EntityID=(.*)\")\n\n        //FULL_ENTITY - Updating [entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1] CardID=HERO_01\n        internal val FULL_ENTITY_PATTERN = Regex(\"FULL_ENTITY - Updating (.*) CardID=(.*)\")\n\n        //[entityName=UNKNOWN ENTITY [cardType=INVALID] id=83 zone=DECK zonePos=0 cardId= player=2]\n        val FULL_ENTITY_CONTENT1_PATTERN =\n            Regex(\"\\\\[entityName=(.*) \\\\[cardType=(.*)] id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]\")\n\n        val FULL_ENTITY_CONTENT2_PATTERN =\n            Regex(\"\\\\[entityName=(.*) id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]\")\n\n        val SHOW_ENTITY = Regex(\"SHOW_ENTITY - Updating Entity=(.*) CardID=(.*)\")\n\n\n        //Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]\n        internal val PLAYER_ENTITY_PATTERN =\n            Regex(\"Player EntityID=(.*) PlayerID=(.*) GameAccountId=(.*)\")\n\n        //BLOCK_START BlockType=ATTACK Entity=[entityName=瓦丝琪女士 id=87 zone=PLAY zonePos=1 cardId=BT_109 player=2]\n        // EffectCardId=System.Collections.Generic.List`1[System.String]\n        // EffectIndex=0 Target=[entityName=驯化的雷象 id=78 zone=PLAY zonePos=1 cardId=SCH_714 player=1] SubOption=-1\n        internal val BLOCK_START_PATTERN =\n            Regex(\"BLOCK_START BlockType=(.*) Entity=(.*) EffectCardId=(.*) EffectIndex=(.*) Target=(.*) SubOption=(.*)\")\n\n        internal val BLOCK_START_CONTINUATION_PATTERN = Regex(\"(.*) TriggerKeyword=(.*)\")\n\n        //BLOCK_END\n        internal val BLOCK_END_PATTERN = Regex(\"BLOCK_END\")\n\n    }\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Tue Jan 18 13:48:58 CST 2022\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.0-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app\"s APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Automatically convert third-party libraries to use AndroidX\nandroid.enableJetifier=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\n#android.jetifier.ignorelist=moshi-1.13.0\nRELEASE_FILE=../123456\nRELEASE_keyAlias=key0\nRELEASE_storePassword=123456\nRELEASE_keyPassword=123456\nandroid.defaults.buildfeatures.buildconfig=true\nandroid.nonTransitiveRClass=false\nandroid.nonFinalResIds=false\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "libs.versions.toml",
    "content": "[versions]\ncompilesdk = \"33\"\nminsdk = \"24\"\ntargetsdk = \"33\"\n\nretrofit = \"2.9.0\"\nconstraintlayout = \"2.1.2\"\nmaterial = \"1.6.0\"\nmoshi = \"1.14.0\"\nktx = \"1.10.0\"\nhilt = \"2.45\"\nlifecycle = \"2.4.0\"\nmmkv = \"1.2.11\"\narouter = \"1.5.2\"\nokhttp = \"4.9.0\"\nnavigation = \"2.3.5\"\nlogger = \"2.2.0\"\ngson = \"2.8.7\"\nappcompat = \"1.6.1\"\nfragment = \"1.5.5\"\nactivity = \"1.6.1\"\nsupport = \"1.0.0\"\nhibinding = \"1.2.0\"\nadapterhelper = \"3.0.4\"\npickerview = \"4.1.9\"\nphotoview = \"2.3.0\"\nglide = \"4.12.0\"\nlatest = \"latest.integration\"\n\n\nkehud = \"1.3.5\"\nkepermission = \"v1.0.0\"\nkemvvm = \"1.1.3\"\nkeimagepicker = \"1.0.1\"\nkewechat = \"1.1.8\"\nkeqrscanner = \"1.1\"\nkeaddresspicker = \"1.0.5\"\nkeapkinstaller = \"1.0.0\"\nkefilepicker = \"1.0.0\"\nkeidcard = \"1.0.6\"\n\nroom = \"2.5.1\"\n\n[libraries]\n\n\nappcompat = { module = \"androidx.appcompat:appcompat\", version.ref = \"appcompat\" }\nmaterial = { module = \"com.google.android.material:material\", version.ref = \"material\" }\nconstraint-layout = { module = \"androidx.constraintlayout:constraintlayout\", version.ref = \"constraintlayout\" }\ncore-ktx = { module = \"androidx.core:core-ktx\", version.ref = \"ktx\" }\nnavigation-fragment = { module = \"androidx.navigation:navigation-fragment-ktx\", version.ref = \"navigation\" }\nnavigation-ui = { module = \"androidx.navigation:navigation-ui-ktx\", version.ref = \"navigation\" }\nfragment-ktx = { module = \"androidx.fragment:fragment-ktx\", version.ref = \"fragment\" }\nactivity = { module = \"androidx.activity:activity\", version.ref = \"activity\" }\nlifecycle-viewmodel-ktx = { module = \"androidx.lifecycle:lifecycle-viewmodel-ktx\", version.ref = \"lifecycle\" }\nlifecycle-livedata-ktx = { module = \"androidx.lifecycle:lifecycle-livedata-ktx\", version.ref = \"lifecycle\" }\nlifecycle-runtime-ktx = { module = \"androidx.lifecycle:lifecycle-runtime-ktx\", version.ref = \"lifecycle\" }\nsupport-v4 = { module = \"androidx.legacy:legacy-support-v4\", version.ref = \"support\" }\n\nroom-runtime = { module = \"androidx.room:room-runtime\",version.ref = \"room\" }\nroom-compiler = { module = \"androidx.room:room-compiler\",version.ref = \"room\" }\nroom-ktx = { module = \"androidx.room:room-ktx\",version.ref = \"room\" }\n\n\nhilt-android = { module = \"com.google.dagger:hilt-android\" , version.ref = \"hilt\"}\nhilt-compiler = { module = \"com.google.dagger:hilt-compiler\" , version.ref = \"hilt\"}\nglide = { module = \"com.github.bumptech.glide:glide\", version.ref = \"glide\" }\nglide-compiler = { module = \"com.github.bumptech.glide:compiler\", version.ref = \"glide\" }\n\nke-mvvm = { module = \"com.github.keluokeda:mvvm_base\", version.ref = \"kemvvm\" }\nlogger = { module = \"com.orhanobut:logger\", version.ref = \"logger\" }\nhi-binding = { module = \"com.hi-dhl:binding\", version.ref = \"hibinding\" }\nmmkv = { module = \"com.tencent:mmkv-static\", version.ref = \"mmkv\" }\n\n\n\nretrofit = { module = \"com.squareup.retrofit2:retrofit\", version.ref = \"retrofit\" }\nretrofit-converter-moshi = { module = \"com.squareup.retrofit2:converter-moshi\", version.ref = \"retrofit\" }\nmoshi = { module = \"com.squareup.moshi:moshi-kotlin\", version.ref = \"moshi\" }\nmoshi-codegen = { module = \"com.squareup.moshi:moshi-kotlin-codegen\", version.ref = \"moshi\" }\n\n\n\nadapter-helper = { module = \"com.github.CymChad:BaseRecyclerViewAdapterHelper\", version.ref = \"adapterhelper\" }\n"
  },
  {
    "path": "module/.gitignore",
    "content": "/build"
  },
  {
    "path": "module/build.gradle",
    "content": "plugins {\n    id 'com.android.library'\n    id 'org.jetbrains.kotlin.android'\n    id 'kotlin-kapt'\n    id 'com.google.dagger.hilt.android'\n    id 'kotlin-parcelize'\n}\nkapt {\n    correctErrorTypes true\n}\nandroid {\n    compileSdk libs.versions.compilesdk.get().toInteger()\n    defaultConfig {\n        minSdk libs.versions.minsdk.get().toInteger()\n        targetSdk libs.versions.targetsdk.get().toInteger()\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n\n        javaCompileOptions {\n            annotationProcessorOptions {\n                arguments += [\n                        \"room.schemaLocation\":\"$projectDir/schemas\".toString(),\n                        \"room.incremental\":\"true\",\n                        \"room.expandProjection\":\"true\"]\n            }\n        }\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_17\n        targetCompatibility JavaVersion.VERSION_17\n    }\n\n\n    resourcePrefix 'module_'\n\n    buildFeatures {\n        viewBinding = true\n    }\n    namespace 'com.ke.hs_tracker.module'\n}\n\ndependencies {\n\n\n    implementation 'androidx.appcompat:appcompat:1.6.1'\n    implementation 'com.google.android.material:material:1.9.0'\n    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'\n    implementation 'androidx.legacy:legacy-support-v4:1.0.0'\n    testImplementation 'junit:junit:4.13.2'\n    testImplementation 'org.junit.jupiter:junit-jupiter'\n    androidTestImplementation 'androidx.test.ext:junit:1.1.3'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'\n\n\n    implementation(libs.core.ktx)\n    implementation(libs.appcompat)\n    implementation(libs.material)\n    implementation(libs.constraint.layout)\n    implementation(libs.fragment.ktx)\n    implementation(libs.activity)\n    implementation(libs.lifecycle.viewmodel.ktx)\n    implementation(libs.lifecycle.livedata.ktx)\n    implementation(libs.lifecycle.runtime.ktx)\n    implementation(libs.support.v4)\n    implementation(libs.logger)\n\n\n    implementation(libs.hilt.android)\n    kapt(libs.hilt.compiler)\n\n    implementation(libs.glide)\n    kapt(libs.glide.compiler)\n\n    implementation(libs.ke.mvvm)\n    implementation(libs.adapter.helper)\n\n    implementation(libs.retrofit)\n    implementation(libs.retrofit.converter.moshi)\n    implementation(libs.moshi)\n    kapt(libs.moshi.codegen)\n\n    implementation(libs.hi.binding)\n    implementation(libs.mmkv)\n\n\n    implementation(libs.room.runtime)\n    implementation(libs.room.ktx)\n    kapt(libs.room.compiler)\n\n//    implementation 'com.tencent.bugly:crashreport:latest.release'\n    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'\n\n    // optional - helpers for implementing LifecycleOwner in a Service\n    implementation \"androidx.lifecycle:lifecycle-service:2.6.1\"\n}"
  },
  {
    "path": "module/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "module/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "module/schemas/com.ke.hs_tracker.module.db.Database/1.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"d83bd3696913c5f4307118a1060d27e4\",\n    \"entities\": [\n      {\n        \"tableName\": \"card\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `name` TEXT NOT NULL, `cost` INTEGER NOT NULL, `id` TEXT NOT NULL, `dbfId` INTEGER NOT NULL, `text` TEXT NOT NULL, `set` TEXT NOT NULL, `type` TEXT NOT NULL, `cardClass` TEXT, `classes` INTEGER NOT NULL, `mechanics` TEXT NOT NULL, `flavor` TEXT NOT NULL, `rarity` TEXT, `durability` INTEGER NOT NULL, `armor` INTEGER NOT NULL, `collectible` INTEGER NOT NULL, `spellSchool` TEXT, `race` TEXT, `attach` INTEGER NOT NULL, `health` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cost\",\n            \"columnName\": \"cost\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"dbfId\",\n            \"columnName\": \"dbfId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"text\",\n            \"columnName\": \"text\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"set\",\n            \"columnName\": \"set\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cardClass\",\n            \"columnName\": \"cardClass\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"classes\",\n            \"columnName\": \"classes\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mechanics\",\n            \"columnName\": \"mechanics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"flavor\",\n            \"columnName\": \"flavor\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rarity\",\n            \"columnName\": \"rarity\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"durability\",\n            \"columnName\": \"durability\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"armor\",\n            \"columnName\": \"armor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"collectible\",\n            \"columnName\": \"collectible\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"spellSchool\",\n            \"columnName\": \"spellSchool\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"race\",\n            \"columnName\": \"race\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"attach\",\n            \"columnName\": \"attach\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"health\",\n            \"columnName\": \"health\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"game\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `build_number` TEXT NOT NULL, `game_type` TEXT NOT NULL, `format_type` TEXT NOT NULL, `scenario_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `opponent_name` TEXT NOT NULL, `is_user_first` INTEGER, `user_deck_name` TEXT NOT NULL, `user_deck_code` TEXT NOT NULL, `is_user_win` INTEGER, `user_hero` TEXT, `opponent_class` TEXT, `start_time` INTEGER NOT NULL, `end_time` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"buildNumber\",\n            \"columnName\": \"build_number\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"gameType\",\n            \"columnName\": \"game_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"formatType\",\n            \"columnName\": \"format_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"scenarioID\",\n            \"columnName\": \"scenario_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userName\",\n            \"columnName\": \"user_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"opponentName\",\n            \"columnName\": \"opponent_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUserFirst\",\n            \"columnName\": \"is_user_first\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"userDeckName\",\n            \"columnName\": \"user_deck_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userDeckCode\",\n            \"columnName\": \"user_deck_code\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUserWin\",\n            \"columnName\": \"is_user_win\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"userHero\",\n            \"columnName\": \"user_hero\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"opponentHero\",\n            \"columnName\": \"opponent_class\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"startTime\",\n            \"columnName\": \"start_time\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"endTime\",\n            \"columnName\": \"end_time\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd83bd3696913c5f4307118a1060d27e4')\"\n    ]\n  }\n}"
  },
  {
    "path": "module/schemas/com.ke.hs_tracker.module.db.Database/2.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 2,\n    \"identityHash\": \"1bb7b04a91def09f83b0a81252d181ba\",\n    \"entities\": [\n      {\n        \"tableName\": \"card\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `name` TEXT NOT NULL, `cost` INTEGER NOT NULL, `id` TEXT NOT NULL, `dbfId` INTEGER NOT NULL, `text` TEXT NOT NULL, `set` TEXT NOT NULL, `type` TEXT NOT NULL, `cardClass` TEXT, `classes` INTEGER NOT NULL, `mechanics` TEXT NOT NULL, `flavor` TEXT NOT NULL, `rarity` TEXT, `durability` INTEGER NOT NULL, `armor` INTEGER NOT NULL, `collectible` INTEGER NOT NULL, `spellSchool` TEXT, `race` TEXT, `attach` INTEGER NOT NULL, `health` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cost\",\n            \"columnName\": \"cost\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"dbfId\",\n            \"columnName\": \"dbfId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"text\",\n            \"columnName\": \"text\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"set\",\n            \"columnName\": \"set\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cardClass\",\n            \"columnName\": \"cardClass\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"classes\",\n            \"columnName\": \"classes\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mechanics\",\n            \"columnName\": \"mechanics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"flavor\",\n            \"columnName\": \"flavor\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rarity\",\n            \"columnName\": \"rarity\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"durability\",\n            \"columnName\": \"durability\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"armor\",\n            \"columnName\": \"armor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"collectible\",\n            \"columnName\": \"collectible\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"spellSchool\",\n            \"columnName\": \"spellSchool\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"race\",\n            \"columnName\": \"race\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"attach\",\n            \"columnName\": \"attach\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"health\",\n            \"columnName\": \"health\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"game\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `build_number` TEXT NOT NULL, `game_type` TEXT NOT NULL, `format_type` TEXT NOT NULL, `scenario_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `opponent_name` TEXT NOT NULL, `is_user_first` INTEGER, `user_deck_name` TEXT NOT NULL, `user_deck_code` TEXT NOT NULL, `is_user_win` INTEGER, `user_hero` TEXT, `opponent_class` TEXT, `start_time` INTEGER NOT NULL, `end_time` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"buildNumber\",\n            \"columnName\": \"build_number\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"gameType\",\n            \"columnName\": \"game_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"formatType\",\n            \"columnName\": \"format_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"scenarioID\",\n            \"columnName\": \"scenario_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userName\",\n            \"columnName\": \"user_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"opponentName\",\n            \"columnName\": \"opponent_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUserFirst\",\n            \"columnName\": \"is_user_first\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"userDeckName\",\n            \"columnName\": \"user_deck_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userDeckCode\",\n            \"columnName\": \"user_deck_code\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUserWin\",\n            \"columnName\": \"is_user_win\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"userHero\",\n            \"columnName\": \"user_hero\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"opponentHero\",\n            \"columnName\": \"opponent_class\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"startTime\",\n            \"columnName\": \"start_time\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"endTime\",\n            \"columnName\": \"end_time\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"in_game_card\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `build_number` TEXT NOT NULL, `card_id` TEXT, `entity_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `zone` TEXT NOT NULL, `turn` INTEGER NOT NULL, `is_user` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"buildNumber\",\n            \"columnName\": \"build_number\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cardId\",\n            \"columnName\": \"card_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"entityId\",\n            \"columnName\": \"entity_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"zone\",\n            \"columnName\": \"zone\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"turn\",\n            \"columnName\": \"turn\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUser\",\n            \"columnName\": \"is_user\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1bb7b04a91def09f83b0a81252d181ba')\"\n    ]\n  }\n}"
  },
  {
    "path": "module/schemas/com.ke.hs_tracker.module.db.Database/3.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 3,\n    \"identityHash\": \"720ca460a50c2d8a7f6bf96c98cf4358\",\n    \"entities\": [\n      {\n        \"tableName\": \"card\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `name` TEXT NOT NULL, `cost` INTEGER NOT NULL, `id` TEXT NOT NULL, `dbfId` INTEGER NOT NULL, `text` TEXT NOT NULL, `set` TEXT NOT NULL, `type` TEXT NOT NULL, `cardClass` TEXT, `classes` INTEGER NOT NULL, `mechanics` TEXT NOT NULL, `flavor` TEXT NOT NULL, `rarity` TEXT, `durability` INTEGER NOT NULL, `armor` INTEGER NOT NULL, `collectible` INTEGER NOT NULL, `spellSchool` TEXT, `race` TEXT, `attach` INTEGER NOT NULL, `health` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cost\",\n            \"columnName\": \"cost\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"dbfId\",\n            \"columnName\": \"dbfId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"text\",\n            \"columnName\": \"text\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"set\",\n            \"columnName\": \"set\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cardClass\",\n            \"columnName\": \"cardClass\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"classes\",\n            \"columnName\": \"classes\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mechanics\",\n            \"columnName\": \"mechanics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"flavor\",\n            \"columnName\": \"flavor\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rarity\",\n            \"columnName\": \"rarity\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"durability\",\n            \"columnName\": \"durability\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"armor\",\n            \"columnName\": \"armor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"collectible\",\n            \"columnName\": \"collectible\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"spellSchool\",\n            \"columnName\": \"spellSchool\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"race\",\n            \"columnName\": \"race\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"attach\",\n            \"columnName\": \"attach\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"health\",\n            \"columnName\": \"health\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"game\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `build_number` TEXT NOT NULL, `game_type` TEXT NOT NULL, `format_type` TEXT NOT NULL, `scenario_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `opponent_name` TEXT NOT NULL, `is_user_first` INTEGER, `user_deck_name` TEXT NOT NULL, `user_deck_code` TEXT NOT NULL, `is_user_win` INTEGER, `user_hero` TEXT, `opponent_class` TEXT, `start_time` INTEGER NOT NULL, `end_time` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"buildNumber\",\n            \"columnName\": \"build_number\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"gameType\",\n            \"columnName\": \"game_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"formatType\",\n            \"columnName\": \"format_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"scenarioID\",\n            \"columnName\": \"scenario_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userName\",\n            \"columnName\": \"user_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"opponentName\",\n            \"columnName\": \"opponent_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUserFirst\",\n            \"columnName\": \"is_user_first\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"userDeckName\",\n            \"columnName\": \"user_deck_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userDeckCode\",\n            \"columnName\": \"user_deck_code\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUserWin\",\n            \"columnName\": \"is_user_win\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"userHero\",\n            \"columnName\": \"user_hero\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"opponentHero\",\n            \"columnName\": \"opponent_class\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"startTime\",\n            \"columnName\": \"start_time\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"endTime\",\n            \"columnName\": \"end_time\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"zone_position_updated_event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `game_id` TEXT NOT NULL, `entity_id` INTEGER NOT NULL, `card_id` TEXT, `card_name` TEXT, `is_user` INTEGER NOT NULL, `current_zone` TEXT NOT NULL, `new_zone` TEXT NOT NULL, `current_position` INTEGER NOT NULL, `new_position` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"gameId\",\n            \"columnName\": \"game_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"entityId\",\n            \"columnName\": \"entity_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cardId\",\n            \"columnName\": \"card_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"cardName\",\n            \"columnName\": \"card_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isUser\",\n            \"columnName\": \"is_user\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"currentZone\",\n            \"columnName\": \"current_zone\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"newZone\",\n            \"columnName\": \"new_zone\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"currentPosition\",\n            \"columnName\": \"current_position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"newPosition\",\n            \"columnName\": \"new_position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '720ca460a50c2d8a7f6bf96c98cf4358')\"\n    ]\n  }\n}"
  },
  {
    "path": "module/schemas/com.ke.hs_tracker.module.db.Database/4.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 4,\n    \"identityHash\": \"a2944b7e136b6abe232a7a446c5ea3db\",\n    \"entities\": [\n      {\n        \"tableName\": \"card\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `name` TEXT NOT NULL, `cost` INTEGER NOT NULL, `id` TEXT NOT NULL, `dbfId` INTEGER NOT NULL, `text` TEXT NOT NULL, `set` TEXT NOT NULL, `type` TEXT NOT NULL, `cardClass` TEXT, `classes` INTEGER NOT NULL, `mechanics` TEXT NOT NULL, `flavor` TEXT NOT NULL, `rarity` TEXT, `durability` INTEGER NOT NULL, `armor` INTEGER NOT NULL, `collectible` INTEGER NOT NULL, `spellSchool` TEXT, `race` TEXT, `attack` INTEGER NOT NULL, `health` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cost\",\n            \"columnName\": \"cost\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"dbfId\",\n            \"columnName\": \"dbfId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"text\",\n            \"columnName\": \"text\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"set\",\n            \"columnName\": \"set\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cardClass\",\n            \"columnName\": \"cardClass\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"classes\",\n            \"columnName\": \"classes\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mechanics\",\n            \"columnName\": \"mechanics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"flavor\",\n            \"columnName\": \"flavor\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rarity\",\n            \"columnName\": \"rarity\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"durability\",\n            \"columnName\": \"durability\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"armor\",\n            \"columnName\": \"armor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"collectible\",\n            \"columnName\": \"collectible\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"spellSchool\",\n            \"columnName\": \"spellSchool\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"race\",\n            \"columnName\": \"race\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"attack\",\n            \"columnName\": \"attack\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"health\",\n            \"columnName\": \"health\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"game\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `build_number` TEXT NOT NULL, `game_type` TEXT NOT NULL, `format_type` TEXT NOT NULL, `scenario_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `opponent_name` TEXT NOT NULL, `is_user_first` INTEGER, `user_deck_name` TEXT NOT NULL, `user_deck_code` TEXT NOT NULL, `is_user_win` INTEGER, `user_hero` TEXT, `opponent_class` TEXT, `start_time` INTEGER NOT NULL, `end_time` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"buildNumber\",\n            \"columnName\": \"build_number\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"gameType\",\n            \"columnName\": \"game_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"formatType\",\n            \"columnName\": \"format_type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"scenarioID\",\n            \"columnName\": \"scenario_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userName\",\n            \"columnName\": \"user_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"opponentName\",\n            \"columnName\": \"opponent_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUserFirst\",\n            \"columnName\": \"is_user_first\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"userDeckName\",\n            \"columnName\": \"user_deck_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userDeckCode\",\n            \"columnName\": \"user_deck_code\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUserWin\",\n            \"columnName\": \"is_user_win\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"userHero\",\n            \"columnName\": \"user_hero\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"opponentHero\",\n            \"columnName\": \"opponent_class\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"startTime\",\n            \"columnName\": \"start_time\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"endTime\",\n            \"columnName\": \"end_time\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"zone_position_updated_event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `game_id` TEXT NOT NULL, `entity_id` INTEGER NOT NULL, `card_id` TEXT, `card_name` TEXT, `is_user` INTEGER NOT NULL, `current_zone` TEXT NOT NULL, `new_zone` TEXT NOT NULL, `current_position` INTEGER NOT NULL, `new_position` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"gameId\",\n            \"columnName\": \"game_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"entityId\",\n            \"columnName\": \"entity_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"cardId\",\n            \"columnName\": \"card_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"cardName\",\n            \"columnName\": \"card_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isUser\",\n            \"columnName\": \"is_user\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"currentZone\",\n            \"columnName\": \"current_zone\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"newZone\",\n            \"columnName\": \"new_zone\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"currentPosition\",\n            \"columnName\": \"current_position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"newPosition\",\n            \"columnName\": \"new_position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a2944b7e136b6abe232a7a446c5ea3db')\"\n    ]\n  }\n}"
  },
  {
    "path": "module/src/androidTest/java/com/ke/hs_tracker/module/ExampleInstrumentedTest.kt",
    "content": "package com.ke.hs_tracker.module\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.ke.hs_tracker.module.test\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "module/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.READ_LOGS\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\" />\n\n    <application\n        android:allowBackup=\"false\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/module_app_name\"\n        android:resizeableActivity=\"true\"\n        android:largeHeap=\"true\"\n        android:theme=\"@style/module_Theme.Hs_tracker\">\n        <activity\n            android:name=\".ui.support.SupportActivity\"\n            android:exported=\"false\">\n\n        </activity>\n\n        <service\n            android:name=\".service.WindowService\"\n            android:enabled=\"true\"\n            android:exported=\"false\" />\n\n        <activity\n            android:name=\".ui.deckbattledetail.DeckBattleDetailActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.classbattledetail.ClassBattleDetailActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.chart.SummaryChartActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.migrate.SocketClientActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.migrate.SocketServerActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.migrate.MigrateMainActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.summary.SummaryActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.test.LocalFileParserActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.zonecards.ZoneCardsActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.zoneevents.ZoneEventsActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.test.CreateRecordActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.records.RecordsActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.writeconfig.WriteConfigActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.diagnose.DiagnoseActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.theme.ThemeActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.filter.FilterActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.test.TestActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.settings.SettingsActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.main.MainActivity\"\n            android:exported=\"false\"\n            android:resizeableActivity=\"true\"\n            android:theme=\"@style/module_Theme.Hs_tracker.Main\" />\n        <activity\n            android:name=\".ui.deck.DeckCodeParserActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.sync.SyncCardDataActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.permissions.PermissionsActivity\"\n            android:exported=\"false\" />\n        <activity\n            android:name=\".ui.splash.SplashActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/module_Theme.Splash\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "module/src/main/assets/Decks.log",
    "content": "I 22:23:04.0780690 Deck Contents Received:\nI 22:23:04.0780690 ### 狗贼\nI 22:23:04.0780690 # Deck ID: 7996212750\nI 22:23:04.0780690 AAEBAZICCukB6awCtLsC1pkDiLED6LoDndgDv+ADpu8DiYsECooOoM0CmNICntIChOYCv/ICj/YCm84D1OgDr4AEAA==\nI 22:23:04.0780690 ### 法术\nI 22:23:04.0780690 # Deck ID: 7997355263\nI 22:23:04.0780690 AAECAf0ECIy5A+DMA7/gA5PhA7L3A6CKBJiNBPyeBAvBuAOBvwPNzgP73QPr3gPQ7APR7AOn9wOKjQT9ngT7ogQA\nI 22:23:04.0780690 ### 自定义 潜行者\nI 22:23:04.0780690 # Deck ID: 8003374434\nI 22:23:04.0780690 AAECAaIHAAAA\nI 22:23:04.0780690 ### 小丑\nI 22:23:04.0780690 # Deck ID: 8004144061\nI 22:23:04.0780690 AAECAZICBrrQA/zeA6bvA9D5A4mLBOSkBAzougObzgPw1AP+2wOJ4AOV4AOi4QOk4QPR4QPm4QPe7AOvgAQA\nI 22:23:04.0780690 ### 双古神\nI 22:23:04.0780690 # Deck ID: 8005951524\nI 22:23:04.0780690 AAECAZICCO66A53YA/zeA4rgA6bvA9D5A4mLBKWNBAvougObzgPw1AOJ4AOV4AOi4QOk4QPR4QPm4QOP5AOvgAQA\nI 22:23:04.0780690 ### 圣契骑\nI 22:23:04.0780690 # Deck ID: 8006192896\nI 22:23:04.0780690 AAECAZ8FDOu5A+u5A+y5A+y5A4TBA5XNA5PQA7/RA/voA5HsA9n5A+CLBAn9uAPquQPKwQPA0QPM6wPw9gON+AO2gAT5pAQA\nI 22:23:04.0780690 ### 对决套牌\nI 22:23:04.0780690 # Deck ID: 8009171311\nI 22:23:04.0780690 AAEBAbLwAw+WBc3OA6TRA9nRA7fSA4vnA9DsA9HsA6f3A673A6CKBIqNBP2eBMWgBPuiBAAA\nI 22:23:04.0780690 ### 任务\nI 22:23:04.0780690 # Deck ID: 8010668041\nI 22:23:04.0780690 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA\nI 22:23:04.0780690 ### 卡扎库\nI 22:23:04.0780690 # Deck ID: 8020766665\nI 22:23:04.0780690 AAECAbr5AwPR4QOJiwSJnwQN5boD6LoD77oDm84D8NQDieADiuADjOQDj+QDr4AErp8EsKUEz6wEAA==\nI 22:23:06.5584800 Deck Contents Received:\nI 22:23:06.5584800 ### 狗贼\nI 22:23:06.5584800 # Deck ID: 7996212750\nI 22:23:06.5584800 AAEBAZICCukB6awCtLsC1pkDiLED6LoDndgDv+ADpu8DiYsECooOoM0CmNICntIChOYCv/ICj/YCm84D1OgDr4AEAA==\nI 22:23:06.5584800 ### 法术\nI 22:23:06.5584800 # Deck ID: 7997355263\nI 22:23:06.5584800 AAECAf0ECIy5A+DMA7/gA5PhA7L3A6CKBJiNBPyeBAvBuAOBvwPNzgP73QPr3gPQ7APR7AOn9wOKjQT9ngT7ogQA\nI 22:23:06.5584800 ### 自定义 潜行者\nI 22:23:06.5584800 # Deck ID: 8003374434\nI 22:23:06.5584800 AAECAaIHAAAA\nI 22:23:06.5584800 ### 小丑\nI 22:23:06.5584800 # Deck ID: 8004144061\nI 22:23:06.5584800 AAECAZICBrrQA/zeA6bvA9D5A4mLBOSkBAzougObzgPw1AP+2wOJ4AOV4AOi4QOk4QPR4QPm4QPe7AOvgAQA\nI 22:23:06.5584800 ### 双古神\nI 22:23:06.5584800 # Deck ID: 8005951524\nI 22:23:06.5584800 AAECAZICCO66A53YA/zeA4rgA6bvA9D5A4mLBKWNBAvougObzgPw1AOJ4AOV4AOi4QOk4QPR4QPm4QOP5AOvgAQA\nI 22:23:06.5584800 ### 圣契骑\nI 22:23:06.5584800 # Deck ID: 8006192896\nI 22:23:06.5584800 AAECAZ8FDOu5A+u5A+y5A+y5A4TBA5XNA5PQA7/RA/voA5HsA9n5A+CLBAn9uAPquQPKwQPA0QPM6wPw9gON+AO2gAT5pAQA\nI 22:23:06.5584800 ### 对决套牌\nI 22:23:06.5584800 # Deck ID: 8009171311\nI 22:23:06.5584800 AAEBAbLwAw+WBc3OA6TRA9nRA7fSA4vnA9DsA9HsA6f3A673A6CKBIqNBP2eBMWgBPuiBAAA\nI 22:23:06.5584800 ### 任务\nI 22:23:06.5584800 # Deck ID: 8010668041\nI 22:23:06.5584800 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA\nI 22:23:06.5584800 ### 卡扎库\nI 22:23:06.5584800 # Deck ID: 8020766665\nI 22:23:06.5584800 AAECAbr5AwPR4QOJiwSJnwQN5boD6LoD77oDm84D8NQDieADiuADjOQDj+QDr4AErp8EsKUEz6wEAA==\nI 22:23:35.6401730 Finding Game With Deck:\nI 22:23:35.6401730 ### 任务\nI 22:23:35.6401730 # Deck ID: 8010668041\nI 22:23:35.6401730 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA\n"
  },
  {
    "path": "module/src/main/assets/log.config",
    "content": "[Bob]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[Power]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[Achievements]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[Arena]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[FullScreenFX]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[LoadingScreen]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[Rachelle]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[Asset]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[Zone]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True\n[Decks]\nLogLevel=1\nFilePrinting=True\nConsolePrinting=False\nScreenPrinting=False\nVerbose=True"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/MainApplication.kt",
    "content": "package com.ke.hs_tracker.module\n\nimport android.app.Application\nimport android.content.Context\nimport android.net.Uri\nimport android.view.LayoutInflater\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.core.content.res.ResourcesCompat\nimport androidx.documentfile.provider.DocumentFile\nimport com.bumptech.glide.Glide\nimport com.ke.hs_tracker.module.data.PreferenceStorage\nimport com.ke.hs_tracker.module.databinding.ModuleDialogCardPreviewBinding\nimport com.ke.hs_tracker.module.databinding.ModuleItemCardBinding\nimport com.ke.hs_tracker.module.entity.Card\nimport com.ke.hs_tracker.module.parser.PowerParserImpl\nimport com.orhanobut.logger.AndroidLogAdapter\nimport com.orhanobut.logger.Logger\nimport com.orhanobut.logger.PrettyFormatStrategy\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.util.Calendar\nimport java.util.Date\nimport javax.inject.Inject\n\n\nabstract class MainApplication : Application() {\n\n\n    @Inject\n    lateinit var preferenceStorage: PreferenceStorage\n\n    override fun onCreate() {\n        super.onCreate()\n//        CrashReport.initCrashReport(applicationContext, \"abb84be20b\", false)\n        AppCompatDelegate.setDefaultNightMode(preferenceStorage.theme)\n        Logger.addLogAdapter(\n            AndroidLogAdapter(\n                PrettyFormatStrategy.newBuilder()\n                    .methodCount(5)\n                    .build()\n            )\n        )\n    }\n}\n\nfun String.removeTime(): Triple<String, Date, String> {\n\n    val content = substring(PowerParserImpl.TIME_PREFIX_SIZE)\n    val start = substring(0, 1)\n    val hms = substring(2, 10).split(\":\")\n    val calendar = Calendar.getInstance()\n    calendar.set(\n        Calendar.HOUR_OF_DAY, hms[0].toInt()\n    )\n    calendar.set(\n        Calendar.MINUTE, hms[1].toInt()\n    )\n    calendar.set(\n        Calendar.SECOND, hms[2].toInt()\n    )\n\n    return Triple(\n        start,\n        calendar.time,\n        content\n    )\n}\n\n\nfun String.log() {\n    Logger.d(this)\n}\n\n/**\n * 是否具备所有权限\n */\nval Context.hasAllPermissions: Boolean\n    get() {\n//        val canWriteExternalStorage = ActivityCompat.checkSelfPermission(\n//            this,\n//            Manifest.permission.WRITE_EXTERNAL_STORAGE\n//        ) == PackageManager.PERMISSION_GRANTED\n//\n//        return canWriteExternalStorage && canReadDataDir && isExternalStorageManager()\n        return canReadDataDir\n    }\n\n/**\n * 是否可以访问data目录\n */\nval Context.canReadDataDir: Boolean\n    get() {\n        return DocumentFile.fromTreeUri(applicationContext, HS_DATA_FILE_DIR)?.canRead() ?: false\n    }\n\n/**\n * 是否具备外部存储管理权限，如果android版本低于11就返回true\n */\n//fun isExternalStorageManager(): Boolean {\n//\n//    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n//        Environment.isExternalStorageManager()\n//    } else {\n//        return true\n//    }\n//}\n\nconst val HS_APPLICATION_ID = \"com.blizzard.wtcg.hearthstone\"\n\n\nval HS_DATA_FILE_DIR =\n    Uri.parse(\"content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata%2F${HS_APPLICATION_ID}\")!!\n\n\n//const val HUAWEI_HS_APPLICATION_ID = \"com.blizzard.wtcg.hearthstone.cn.huawei\"\n\n/**\n * 写入log.config文件\n */\nsuspend fun Context.writeLogConfigFile(forceWrite: Boolean = false): Boolean {\n    return withContext(Dispatchers.IO) {\n\n\n        val documentFile = findHSDataFilesDir() ?: return@withContext false\n\n        val fileName = \"log.config\"\n        val file = findHSDataFilesDir(fileName)\n        if (file != null) {\n            //文件已存在\n            \"log.config文件已存在\".log()\n            if (forceWrite) {\n                file.delete()\n            } else {\n                return@withContext true\n            }\n        }\n        val configFile = documentFile.createFile(\"plain/text\", fileName)\n            ?: return@withContext false\n\n        contentResolver.openOutputStream(configFile.uri)?.use {\n            assets.open(\"log.config\")\n                .copyTo(it)\n            it.flush()\n        }\n\n        return@withContext true\n    }\n\n\n}\n\n//通过从根目录进入的方式可以创建文件\nfun Context.findHSDataFilesDir(\n\n    fileName: String? = null,\n\n    ): DocumentFile? {\n\n    DocumentFile.fromTreeUri(\n        applicationContext,\n        HS_DATA_FILE_DIR\n    )?.apply {\n//        listFiles().forEach {\n//            if (it.name == applicationId) {\n        val filesDir = this.findFile(\"files\") ?: return null\n\n\n\n        return if (fileName == null) filesDir else filesDir.findFile(\n            fileName\n        )\n//            }\n//        }\n    }\n\n    return null\n\n//    if (applicationId == HUAWEI_HS_APPLICATION_ID) {\n//        return null\n//    }\n//\n//    return findHSDataFilesDir(fileName, HUAWEI_HS_APPLICATION_ID)\n\n\n}\n\nfun ModuleItemCardBinding.bindCard(card: Card) {\n    name.text = card.name\n    cost.text = card.cost.toString()\n\n    card.rarity?.apply {\n        this@bindCard.cost.setBackgroundColor(\n            ResourcesCompat.getColor(\n                root.context.resources,\n                colorRes,\n                null\n            )\n        )\n\n    }\n\n    Glide.with(imageTile)\n        .load(\"https://art.hearthstonejson.com/v1/tiles/${card.id}.png\")\n        .into(imageTile)\n}\n\n\nfun showCardImageDialog(context: Context, cardId: String) {\n    val binding = ModuleDialogCardPreviewBinding.inflate(LayoutInflater.from(context))\n    AlertDialog.Builder(context)\n        .show().apply {\n            window?.run {\n                setContentView(binding.root)\n                binding.root.setOnClickListener {\n                    dismiss()\n                }\n                //去掉对话框的白色背景\n                setBackgroundDrawableResource(android.R.color.transparent)\n            }\n        }\n    Glide.with(binding.image)\n        .load(\"https://art.hearthstonejson.com/v1/render/latest/zhCN/512x/${cardId}.png\")\n        .placeholder(R.mipmap.ic_launcher)\n        .into(binding.image)\n}\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/api/HearthStoneJsonApi.kt",
    "content": "package com.ke.hs_tracker.module.api\n\nimport com.ke.hs_tracker.module.entity.Card\nimport retrofit2.http.GET\nimport retrofit2.http.Path\n\ninterface HearthStoneJsonApi {\n\n\n    /**\n     * 获取卡牌数据\n     */\n    @GET(\"v1/{versionCode}/{region}/cards.json\")\n    suspend fun getCardJsonList(\n        @Path(\"versionCode\") versionCode: String,\n        @Path(\"region\") region: String,\n    ): List<Card>\n\n\n    companion object {\n        const val BASE_URL = \"https://api.hearthstonejson.com/\"\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/data/PreferenceStorage.kt",
    "content": "package com.ke.hs_tracker.module.data\n\nimport android.content.Context\nimport androidx.appcompat.app.AppCompatDelegate\nimport com.tencent.mmkv.MMKV\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport javax.inject.Inject\n\ninterface PreferenceStorage {\n\n    var theme: Int\n\n    /**\n     * 保存日志文件\n     */\n    var saveLogFile: Boolean\n\n    var floatingEnable: Boolean\n\n    var crash: String?\n}\n\nclass PreferenceStorageImpl @Inject constructor(\n    @ApplicationContext private val context: Context\n) : PreferenceStorage {\n\n    init {\n        MMKV.initialize(context)\n    }\n\n    private val mmkv = MMKV.defaultMMKV()\n\n\n    override var theme: Int\n        get() = mmkv.getInt(KEY_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)\n        set(value) {\n            AppCompatDelegate.setDefaultNightMode(value)\n            mmkv.encode(KEY_THEME, value)\n        }\n\n    override var saveLogFile: Boolean\n        get() = mmkv.getBoolean(KEY_SAVE_LOG_FILE, false)\n        set(value) {\n            mmkv.encode(KEY_SAVE_LOG_FILE, value)\n        }\n\n    override var floatingEnable: Boolean\n        get() = mmkv.getBoolean(KEY_FLOATING_ENABLE, true)\n        set(value) {\n            mmkv.putBoolean(KEY_FLOATING_ENABLE, value)\n        }\n\n    override var crash: String?\n        get() = mmkv.getString(KEY_CRASH, null)\n        set(value) {\n            mmkv.putString(KEY_CRASH, value)\n        }\n\n    companion object {\n        private const val KEY_THEME = \"KEY_THEME\"\n        private const val KEY_SAVE_LOG_FILE = \"KEY_SAVE_LOG_FILE\"\n        private const val KEY_FLOATING_ENABLE = \"KEY_FLOATING_ENABLE\"\n        private const val KEY_CRASH = \"KEY_CRASH\"\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/db/CardClassesConvert.kt",
    "content": "package com.ke.hs_tracker.module.db\n\nimport androidx.room.TypeConverter\nimport com.ke.hs_tracker.module.entity.CardClass\n\nclass CardClassesConvert {\n\n    @TypeConverter\n    fun longToClasses(value: Long?): List<CardClass> {\n        if (value == null) {\n            return emptyList()\n        }\n        return CardClass.values()\n            .map {\n                it to (1L shl it.ordinal)\n            }\n            .filter {\n                it.second and value == it.second\n            }\n            .map { it.first }\n    }\n\n    @TypeConverter\n    fun classesToLong(list: List<CardClass>): Long {\n        if (list.isEmpty()) {\n            return 0\n        }\n        var result = 0L\n        list.map {\n            1L shl it.ordinal\n        }.forEach {\n            result = result or it\n        }\n\n        return result\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/db/CardDao.kt",
    "content": "package com.ke.hs_tracker.module.db\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.Query\nimport com.ke.hs_tracker.module.entity.Card\n\n@Dao\ninterface CardDao {\n\n    @Query(\"select * from card\")\n    suspend fun getAll(): List<Card>\n\n    @Insert\n    suspend fun insert(list: List<Card>)\n\n    @Query(\"delete from card\")\n    suspend fun deleteAll()\n\n    @Query(\"select COUNT(id) from card\")\n    suspend fun getCount(): Int\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/db/Database.kt",
    "content": "package com.ke.hs_tracker.module.db\n\nimport androidx.room.*\nimport androidx.room.Database\nimport androidx.room.migration.AutoMigrationSpec\nimport com.ke.hs_tracker.module.entity.Card\n\n\nconst val DATABASE_VERSION = 4\n\n@Database(\n    entities = [Card::class, Game::class, ZonePositionChangedEvent::class],\n    version = DATABASE_VERSION,\n    exportSchema = true,\n    autoMigrations = [\n        AutoMigration(\n            from = 3,\n            to = 4,\n            spec = com.ke.hs_tracker.module.db.Database.RenameAttachToAttackMigration::class\n        )\n    ]\n\n)\n@TypeConverters(\n    CardClassesConvert::class, MechanicsListConvert::class\n)\nabstract class Database : RoomDatabase() {\n\n    @RenameColumn(tableName = \"card\", fromColumnName = \"attach\", toColumnName = \"attack\")\n    class RenameAttachToAttackMigration : AutoMigrationSpec\n\n\n    abstract fun cardDao(): CardDao\n\n    abstract fun gameDao(): GameDao\n\n\n    abstract fun zonePositionChangedEventDao(): ZonePositionChangedEventDao\n\n    companion object {\n        const val VERSION = 3\n    }\n}\n\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/db/Game.kt",
    "content": "package com.ke.hs_tracker.module.db\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.hs_tracker.module.entity.FormatType\nimport com.ke.hs_tracker.module.entity.GameType\nimport com.squareup.moshi.JsonClass\nimport java.util.*\n\n@JsonClass(generateAdapter = true)\n@Entity(tableName = \"game\")\ndata class Game(\n    @PrimaryKey(autoGenerate = false)\n    val id: String = UUID.randomUUID().toString(),\n    //并不是唯一标识\n    @ColumnInfo(name = \"build_number\")\n    val buildNumber: String = \"\",\n    @ColumnInfo(name = \"game_type\")\n    var gameType: GameType = GameType.Unknown,\n    @ColumnInfo(name = \"format_type\")\n    var formatType: FormatType = FormatType.Unknown,\n    @ColumnInfo(name = \"scenario_id\")\n    var scenarioID: Int = 0,\n    @ColumnInfo(name = \"user_name\")\n    var userName: String = \"\",\n    @ColumnInfo(name = \"opponent_name\")\n    var opponentName: String = \"\",\n    @ColumnInfo(name = \"is_user_first\")\n    var isUserFirst: Boolean? = null,\n    @ColumnInfo(name = \"user_deck_name\")\n    var userDeckName: String = \"\",\n    @ColumnInfo(name = \"user_deck_code\")\n    var userDeckCode: String = \"\",\n    @ColumnInfo(name = \"is_user_win\")\n    var isUserWin: Boolean? = null,\n    @ColumnInfo(name = \"user_hero\")\n    var userHero: CardClass? = null,\n    @ColumnInfo(name = \"opponent_class\")\n    var opponentHero: CardClass? = null,\n    @ColumnInfo(name = \"start_time\")\n    var startTime: Long = 0,\n    @ColumnInfo(name = \"end_time\")\n    var endTime: Long = 0\n) {\n\n//    val userName: String\n//        get() = if (isUserFirst == true) player1Name else player2Name\n\n//    var opponentName: String\n//        get() = if (isUserFirst == true) player2Name else player1Name\n//        set(value) {\n//            if (isUserFirst == true) {\n//                player2Name = value\n//            } else {\n//                player1Name = value\n//            }\n//        }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/db/GameDao.kt",
    "content": "package com.ke.hs_tracker.module.db\n\nimport androidx.room.*\nimport com.ke.hs_tracker.module.entity.CardClass\n\n@Dao\ninterface GameDao {\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(game: Game)\n\n\n    /**\n     * 插入所有\n     */\n    @Insert(onConflict = OnConflictStrategy.ABORT)\n    suspend fun insert(games: List<Game>)\n\n    @Update\n    suspend fun update(game: Game)\n\n    /**\n     * 删除全部\n     */\n    @Query(\"delete from game\")\n    suspend fun deleteAll()\n\n    /**\n     * 查询所有\n     */\n    @Query(\"select * from game\")\n    suspend fun getAll(): List<Game>\n\n    /**\n     * 查找用户某个英雄的总对局\n     */\n    @Query(\"select * from game where user_hero = :cardClass\")\n    suspend fun getByHero(cardClass: CardClass): List<Game>\n\n    /**\n     * 获取总的对局数\n     */\n    @Query(\"select count(*) from game\")\n    suspend fun getGameCount(): Int\n\n    /**\n     * 获取玩家胜率对局数\n     */\n    @Query(\"select count(*) from game where is_user_win = 1\")\n    suspend fun getUserWinCount(): Int\n\n    /**\n     * 根据卡组代码和名称查询\n     */\n    @Query(\"select * from game where user_deck_code = :code and user_deck_name = :name\")\n    suspend fun getByDeckCodeAndName(\n        code: String,\n        name: String\n    ): List<Game>\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/db/MechanicsListConvert.kt",
    "content": "package com.ke.hs_tracker.module.db\n\nimport androidx.room.TypeConverter\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.hs_tracker.module.entity.Mechanics\nimport com.ke.hs_tracker.module.entity.MechanicsAdapter\nimport com.squareup.moshi.JsonAdapter\nimport com.squareup.moshi.Moshi\nimport com.squareup.moshi.adapter\nimport org.json.JSONArray\nimport javax.inject.Inject\n\nclass MechanicsListConvert {\n\n    @TypeConverter\n    fun stringToClasses(value: String?): List<Mechanics> {\n        if (value == null) {\n            return emptyList()\n        }\n        val result = mutableListOf<Mechanics>()\n\n        val jsonArray = JSONArray(value)\n\n        val mechanicsAdapter = MechanicsAdapter()\n\n        val size = jsonArray.length()\n        for (index in 0 until size) {\n            val m = mechanicsAdapter.fromJson(jsonArray.get(index).toString())\n            result.add(m)\n        }\n        return result\n\n//        return Mechanics.values()\n//            .map {\n//                it to (1L shl it.ordinal)\n//            }\n//            .filter {\n//                it.second and value == it.second\n//            }\n//            .map { it.first }\n    }\n\n    @TypeConverter\n    fun classesToString(list: List<Mechanics>): String {\n        val jsonArray = JSONArray()\n        if (list.isEmpty()) {\n            return jsonArray.toString()\n        }\n\n        list.forEach {\n            jsonArray.put(it.name)\n        }\n\n        return jsonArray.toString()\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/db/ZonePositionChangedEvent.kt",
    "content": "package com.ke.hs_tracker.module.db\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport com.ke.hs_tracker.module.entity.Zone\nimport com.squareup.moshi.JsonClass\nimport java.util.*\n\n@JsonClass(generateAdapter = true)\n@Entity(tableName = \"zone_position_updated_event\")\ndata class ZonePositionChangedEvent(\n    @PrimaryKey(autoGenerate = false)\n    val id: String = UUID.randomUUID().toString(),\n    @ColumnInfo(name = \"game_id\")\n    var gameId: String = \"\",\n    @ColumnInfo(name = \"entity_id\")\n    val entityId: Int,\n    @ColumnInfo(name = \"card_id\")\n    var cardId: String?,\n    @ColumnInfo(name = \"card_name\")\n    var cardName: String? = null,\n    @ColumnInfo(name = \"is_user\")\n    var isUser: Boolean = false,\n    @ColumnInfo(name = \"current_zone\")\n    var currentZone: Zone,\n    @ColumnInfo(name = \"new_zone\")\n    var newZone: Zone,\n    @ColumnInfo(name = \"current_position\")\n    val currentPosition: Int,\n    @ColumnInfo(name = \"new_position\")\n    val newPosition: Int\n) {\n\n    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] tag=ZONE value=HAND\n    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] tag=ZONE_POSITION value=1\n    fun plus(event: ZonePositionChangedEvent): ZonePositionChangedEvent {\n        if (event.entityId != entityId) {\n            throw RuntimeException(\"两个要进行加的 entity id 不一致 $event\")\n        }\n\n        if (currentZone == Zone.Deck && newZone == Zone.Deck && newPosition != 0) {\n            //星界导致插入一张卡牌到事件中\n            currentZone = event.newZone\n            newZone = event.newZone\n        }\n\n\n        if (currentZone != event.currentZone) {\n            throw RuntimeException(\"两个要进行加的 zone 不一致  $event\")\n        }\n\n        val newPosAndZone =\n            if (this.currentZone == event.newZone) event.newPosition to newZone else newPosition to event.newZone\n\n        return ZonePositionChangedEvent(\n            id,\n            gameId,\n            entityId,\n            cardId,\n            cardName,\n            isUser,\n            currentZone,\n            newPosAndZone.second,\n            currentPosition,\n            newPosAndZone.first\n        )\n    }\n\n    fun plusPlus(\n        second: ZonePositionChangedEvent,\n        third: ZonePositionChangedEvent\n    ): ZonePositionChangedEvent {\n        val oldZone = currentZone\n        val list = listOf(this, second, third)\n        val newZone = list.map {\n            it.newZone\n        }.firstOrNull {\n            it != oldZone\n        }\n\n        val newPosition = third.newPosition\n\n        return ZonePositionChangedEvent(\n            id,\n            gameId,\n            entityId,\n            cardId,\n            cardName,\n            isUser,\n            currentZone,\n            newZone ?: this.newZone,\n            currentPosition,\n            newPosition\n        )\n    }\n}\n\n//public operator fun ZonePositionChangedEvent.plus(event: ZonePositionChangedEvent): ZonePositionChangedEvent {\n//    if (entityId != event.entityId) {\n//        throw RuntimeException(\"不支持不同 entityId 的 event 相加\")\n//    }\n//    if (this == event) {\n//        return this\n//    }\n//}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/db/ZonePositionChangedEventDao.kt",
    "content": "package com.ke.hs_tracker.module.db\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.Query\n\n@Dao\ninterface ZonePositionChangedEventDao {\n\n    @Insert\n    suspend fun insertAll(list: List<ZonePositionChangedEvent>)\n\n    @Query(\"select * from zone_position_updated_event where game_id = :gameId\")\n    suspend fun getAllByGameId(gameId: String): List<ZonePositionChangedEvent>\n\n    /**\n     * 获取所有\n     */\n    @Query(\"select * from zone_position_updated_event\")\n    suspend fun getAll(): List<ZonePositionChangedEvent>\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/di/CoroutinesModule.kt",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.ke.hs_tracker.module.di\n\n\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.components.SingletonComponent\nimport kotlinx.coroutines.CoroutineDispatcher\nimport kotlinx.coroutines.Dispatchers\n\n@InstallIn(SingletonComponent::class)\n@Module\nobject CoroutinesModule {\n\n    @DefaultDispatcher\n    @Provides\n    fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default\n\n    @IoDispatcher\n    @Provides\n    fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO\n\n    @MainDispatcher\n    @Provides\n    fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main\n\n    @MainImmediateDispatcher\n    @Provides\n    fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate\n}\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/di/CoroutinesQualifiers.kt",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.ke.hs_tracker.module.di\n\nimport javax.inject.Qualifier\n\n@Retention(AnnotationRetention.BINARY)\n@Qualifier\nannotation class DefaultDispatcher\n\n@Retention(AnnotationRetention.BINARY)\n@Qualifier\nannotation class IoDispatcher\n\n@Retention(AnnotationRetention.BINARY)\n@Qualifier\nannotation class MainDispatcher\n\n@Retention(AnnotationRetention.BINARY)\n@Qualifier\nannotation class MainImmediateDispatcher\n\n@Retention(AnnotationRetention.BINARY)\n@Qualifier\nannotation class ApplicationScope\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/di/LogFileDirQualifiers.kt",
    "content": "/*\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.ke.hs_tracker.module.di\n\nimport javax.inject.Qualifier\n\n\n@Retention(AnnotationRetention.BINARY)\n@Qualifier\nannotation class LocalLogFileDir\n\n@Retention(AnnotationRetention.BINARY)\n@Qualifier\nannotation class RealLogFileDir\n\n\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/di/Module.kt",
    "content": "package com.ke.hs_tracker.module.di\n\nimport android.content.Context\nimport androidx.room.Room\nimport com.ke.hs_tracker.module.api.HearthStoneJsonApi\nimport com.ke.hs_tracker.module.data.PreferenceStorage\nimport com.ke.hs_tracker.module.data.PreferenceStorageImpl\nimport com.ke.hs_tracker.module.db.*\nimport com.ke.hs_tracker.module.entity.*\nimport com.ke.hs_tracker.module.parser.*\nimport com.squareup.moshi.Moshi\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport okhttp3.OkHttpClient\nimport retrofit2.Retrofit\nimport retrofit2.converter.moshi.MoshiConverterFactory\nimport java.util.concurrent.TimeUnit\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nclass Module {\n\n    @Provides\n    @Singleton\n    fun provideHttpClient(): OkHttpClient {\n        return OkHttpClient.Builder()\n            .readTimeout(30, TimeUnit.SECONDS)\n            .writeTimeout(30, TimeUnit.SECONDS)\n            .connectTimeout(30, TimeUnit.SECONDS)\n            .build()\n    }\n\n    @Provides\n    @Singleton\n    fun provideMoshi(): Moshi {\n        return Moshi.Builder()\n            .add(CardClassAdapter())\n            .add(CardTypeAdapter())\n            .add(RarityAdapter())\n            .add(SpellSchoolAdapter())\n            .add(MechanicsAdapter())\n            .add(RaceAdapter())\n            .build()\n    }\n\n    @Provides\n    @Singleton\n    fun provideHearthStoneJsonApi(\n        okHttpClient: OkHttpClient,\n        moshi: Moshi\n    ): HearthStoneJsonApi {\n\n        return Retrofit.Builder()\n            .addConverterFactory(MoshiConverterFactory.create(moshi))\n            .client(okHttpClient)\n            .baseUrl(HearthStoneJsonApi.BASE_URL)\n            .build()\n            .create(HearthStoneJsonApi::class.java)\n    }\n\n    @Provides\n    @Singleton\n    fun provideDatabase(@ApplicationContext context: Context): Database {\n        return Room.databaseBuilder(\n            context,\n            Database::class.java,\n            \"card.db\"\n        ).build()\n    }\n\n    @Provides\n    fun provideCardDao(database: Database): CardDao {\n        return database.cardDao()\n    }\n\n    @Provides\n    fun provideGameDao(database: Database): GameDao = database.gameDao()\n\n\n    @Provides\n    fun provideZonePositionChangedEventDao(database: Database): ZonePositionChangedEventDao =\n        database.zonePositionChangedEventDao()\n\n    @Provides\n    @Singleton\n    fun providePreferenceStorage(preferenceStorageImpl: PreferenceStorageImpl): PreferenceStorage {\n        return preferenceStorageImpl\n    }\n\n    @Provides\n    fun provideBlockTagStack(impl: BlockTagStackImpl): BlockTagStack {\n        return impl\n    }\n\n    @Provides\n    fun providePowerParser(powerParserImpl: PowerParserImpl): PowerParser {\n        return powerParserImpl\n    }\n\n    @Provides\n    fun providePowerTagHandler(powerTagHandlerImpl: PowerTagHandlerImpl): PowerTagHandler =\n        powerTagHandlerImpl\n\n    @Provides\n    fun provideDeckCardObserver(deckCardObserverImpl: DeckCardObserverImpl): DeckCardObserver =\n        deckCardObserverImpl\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/ClearCardTableUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport com.ke.hs_tracker.module.db.CardDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass ClearCardTableUseCase @Inject constructor(\n    private val cardDao: CardDao,\n    @IoDispatcher dispatcher: CoroutineDispatcher\n) : UseCase<Unit, Unit>(dispatcher) {\n    override suspend fun execute(parameters: Unit) {\n        cardDao.deleteAll()\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/GetAllCardUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport com.ke.hs_tracker.module.db.CardDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.entity.Card\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass GetAllCardUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    private val cardDao: CardDao\n) :\n    UseCase<Unit, List<Card>>(dispatcher) {\n\n    override suspend fun execute(parameters: Unit): List<Card> {\n        return cardDao.getAll()\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/GetCardListUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport com.ke.hs_tracker.module.api.HearthStoneJsonApi\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.entity.Card\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass GetCardListUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    private val api: HearthStoneJsonApi\n) :\n    UseCase<Pair<String, String>, List<Card>>(dispatcher) {\n\n    override suspend fun execute(parameters: Pair<String, String>): List<Card> {\n        return api.getCardJsonList(\n            parameters.first,\n            parameters.second\n        )\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/GetDatabaseCardCountUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport com.ke.hs_tracker.module.db.CardDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass GetDatabaseCardCountUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    private val cardDao: CardDao\n) : UseCase<Unit, Int>(dispatcher) {\n\n    override suspend fun execute(parameters: Unit): Int {\n        return cardDao.getCount()\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/GetLocalLogDirUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport android.content.Context\nimport androidx.documentfile.provider.DocumentFile\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.findHSDataFilesDir\nimport com.ke.mvvm.base.domian.UseCase\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.CoroutineDispatcher\nimport java.io.File\nimport javax.inject.Inject\n\nclass GetLocalLogDirUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    @ApplicationContext private val context: Context\n) :\n    UseCase<Unit, DocumentFile?>(dispatcher) {\n\n    override suspend fun execute(parameters: Unit): DocumentFile? {\n        val logsDir = context.getExternalFilesDir(\"Logs\")!!\n        if (!logsDir.exists()) {\n            logsDir.mkdir()\n\n        }\n//        val decksFile = File(logsDir, \"Decks.log\")\n//        decksFile.createNewFile()\n//        context.assets.open(\"Decks.log\").reader()\n//            .apply {\n//                decksFile.writeText(readText())\n//            }\n\n        val powerFile = File(logsDir, \"Power.log\")\n        powerFile.createNewFile()\n\n        return context.findHSDataFilesDir(\"Logs\")\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/GetRealLogDirUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport android.content.Context\nimport androidx.documentfile.provider.DocumentFile\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.findHSDataFilesDir\nimport com.ke.hs_tracker.module.log\nimport com.ke.mvvm.base.domian.UseCase\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass GetRealLogDirUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    @ApplicationContext private val context: Context\n) :\n    UseCase<Unit, DocumentFile?>(dispatcher) {\n\n//    private var documentFile: DocumentFile? = null\n\n    override suspend fun execute(parameters: Unit): DocumentFile? {\n\n//        if (documentFile != null) {\n//            return documentFile\n//        }\n\n        val logsDir = context.findHSDataFilesDir(\"Logs\")\n            ?: return null\n\n        val listFiles = logsDir.listFiles()\n\n\n\n        return listFiles.filter {\n            \"${it.name} ${it.lastModified()}\".log()\n\n            (it.name?.startsWith(\"Hearthstone\") ?: false) && it.isDirectory\n        }.maxByOrNull {\n            it.lastModified()\n        }?.apply {\n            \"找到了目标目录 ${this.name} ${this.lastModified()}\".log()\n        }\n\n\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/GetSaveLogFileEnableUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport com.ke.hs_tracker.module.data.PreferenceStorage\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass GetSaveLogFileEnableUseCase @Inject constructor(\n    private val preferenceStorage: PreferenceStorage,\n    @IoDispatcher dispatcher: CoroutineDispatcher\n) :\n    UseCase<Unit, Boolean>(dispatcher) {\n\n    override suspend fun execute(parameters: Unit): Boolean {\n        return preferenceStorage.saveLogFile\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/InsertCardListToDatabaseUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport com.ke.hs_tracker.module.db.CardDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.entity.Card\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass InsertCardListToDatabaseUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    private val cardDao: CardDao\n) : UseCase<List<Card>, Unit>(dispatcher) {\n    override suspend fun execute(parameters: List<Card>) {\n        cardDao.insert(parameters)\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/ParseDeckCodeUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport android.util.Base64\nimport com.ke.hs_tracker.module.db.CardDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.entity.Card\nimport com.ke.hs_tracker.module.entity.CardBean\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass ParseDeckCodeUseCase\n@Inject constructor(\n    private val cardDao: CardDao,\n    @IoDispatcher dispatcher: CoroutineDispatcher\n) : UseCase<String, List<CardBean>>(dispatcher) {\n    override suspend fun execute(parameters: String): List<CardBean> {\n        val allCards = cardDao.getAll()\n\n        val byteArray =\n            Base64.decode(parameters, Base64.DEFAULT)\n//                deckString.encodeToByteArray().decodeBase64()\n        //[0, 1, 2, 1, -3, 4, 8, -116, -71, 3, -32, -52, 3, -65, -32, 3, -109, -31, 3, -78, -9, 3, -96, -118, 4, -104, -115, 4, -4, -98, 4, 11, -63, -72, 3, -127, -65, 3, -51, -50, 3, -5, -35, 3, -21, -34, 3, -48, -20, 3, -47, -20, 3, -89, -9, 3, -118, -115, 4, -3, -98, 4, -5, -94, 4, 0]\n//        val size = byteArray.size\n        val byteList = mutableListOf<Byte>()\n        byteArray.forEach {\n            byteList.add(it)\n        }\n\n        val keep = byteList.removeFirst()//移除第一个保留的字段\n//            assert(byteList.removeFirst().toInt() == 0)\n//            assert(byteList.removeFirst().toInt() != 0)//总是1\n        val version = byteList.removeFirst()\n        val cardType = byteList.removeFirst()//1是标准 2是狂野\n        val cardList = mutableListOf<CardBean>()\n        val heroCount = getVarInt(byteList)\n        for (i in 0 until heroCount) {\n            val id = getVarInt(byteList)\n            val hero =\n                findByDbfId(id, allCards) ?: throw IllegalArgumentException(\"找不到id为 $id  的卡牌\")\n//            hero.count = heroCount\n            cardList.add(hero.updateCount(heroCount))\n        }\n\n        for (i in 1..3) {\n            val c = getVarInt(byteList)\n            for (j in 0 until c) {\n                val dbfId = getVarInt(byteList)\n                val count: Int\n                if (i == 3) {\n                    count = getVarInt(byteList)\n                } else {\n                    count = i\n                }\n//                    result.cards.add(Card(dbfId, count))\n                val jsonObject =\n                    findByDbfId(dbfId, allCards)\n                        ?: throw IllegalArgumentException(\"找不到id为 $dbfId  的卡牌\")\n//                jsonObject.count = count\n                cardList.add(jsonObject.updateCount(count))\n            }\n        }\n\n        //移除英雄\n        cardList.removeFirst()\n\n        cardList.sortBy {\n            it.card.cost\n        }\n\n        return cardList\n    }\n\n    /**\n     * 获得无符号int\n     */\n    private fun getVarInt(src: MutableList<Byte>): Int {\n        var result = 0\n        var shift = 0\n        var b: Int\n\n        do {\n            if (shift >= 32) {\n                // Out of range\n                throw IndexOutOfBoundsException(\"varint too long\")\n            }\n            // Get 7 bits from next byte\n            b = src.removeFirst().toInt()\n            result = result or (b and 0x7F shl shift)\n            shift += 7\n        } while (b and 0x80 != 0)\n        return result\n    }\n\n    private fun findByDbfId(id: Int, cardEntityList: List<Card>): CardBean? {\n        val cardEntity = cardEntityList.find { it.dbfId == id } ?: return null\n\n        return CardBean(cardEntity)\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/SaveLogFileUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport android.content.Context\nimport android.net.Uri\nimport com.ke.hs_tracker.module.data.PreferenceStorage\nimport com.ke.mvvm.base.domian.UseCase\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport java.io.File\nimport java.io.InputStream\nimport javax.inject.Inject\n\nclass SaveLogFileUseCase @Inject constructor(\n    private val preferenceStorage: PreferenceStorage,\n    @ApplicationContext private val context: Context\n) :\n    UseCase<Pair<String, InputStream>, Boolean>(Dispatchers.IO) {\n    override suspend fun execute(parameters: Pair<String, InputStream>): Boolean {\n        if (!preferenceStorage.saveLogFile) {\n            return false\n        }\n        val targetFileDir = File(context.getExternalFilesDir(null), \"logs\")\n        if (!targetFileDir.exists()) {\n            targetFileDir.mkdir()\n        }\n        val target = File(targetFileDir, parameters.first + \".log\")\n        if (!target.exists()) {\n            target.createNewFile()\n        }\n        val text = parameters.second.bufferedReader().readText()\n\n        target.writeText(text)\n\n        return true\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/SetSaveLogFileEnableUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport com.ke.hs_tracker.module.data.PreferenceStorage\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass SetSaveLogFileEnableUseCase @Inject constructor(\n    private val preferenceStorage: PreferenceStorage,\n    @IoDispatcher dispatcher: CoroutineDispatcher\n) :\n    UseCase<Boolean, Boolean>(dispatcher) {\n\n    override suspend fun execute(parameters: Boolean): Boolean {\n        preferenceStorage.saveLogFile = parameters\n        return parameters\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/domain/WriteLogConfigFileUseCase.kt",
    "content": "package com.ke.hs_tracker.module.domain\n\nimport android.content.Context\nimport androidx.documentfile.provider.DocumentFile\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.findHSDataFilesDir\nimport com.ke.hs_tracker.module.log\nimport com.ke.hs_tracker.module.writeLogConfigFile\nimport com.ke.mvvm.base.domian.UseCase\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass WriteLogConfigFileUseCase @Inject constructor(\n    @ApplicationContext private val context: Context,\n    @IoDispatcher private val dispatcher: CoroutineDispatcher\n) :\n    UseCase<Boolean, Boolean>(dispatcher) {\n\n    override suspend fun execute(parameters: Boolean): Boolean {\n\n        return context.writeLogConfigFile(parameters)\n//        val documentFile = context.findHSDataFilesDir() ?: return false\n//\n//        val fileName = \"log.config\"\n//        val file = context.findHSDataFilesDir(fileName)\n//        if (file != null) {\n//            //文件已存在\n//            \"log.config文件已存在\".log()\n//\n//            if (parameters) {\n//                file.delete()\n//                val configFile = documentFile.createFile(\"plain/text\", fileName)\n//                    ?: return false\n//\n//                write(configFile)\n//            } else {\n//                return true\n//            }\n//        }\n//        val configFile = documentFile.createFile(\"plain/text\", fileName)\n//            ?: return false\n//\n//        write(configFile)\n//\n//        return true\n    }\n\n    private fun write(configFile: DocumentFile) {\n        context.contentResolver.openOutputStream(configFile.uri)?.use {\n            context.assets.open(\"log.config\")\n                .copyTo(it)\n            it.flush()\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/BlockType.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nenum class BlockType {\n    /**\n     * 攻击\n     */\n    Attack,\n\n    /**\n     * 死亡\n     */\n    Deaths,\n\n    /**\n     * 触发\n     */\n    Trigger,\n\n    /**\n     * 打出一张卡牌\n     */\n    Play,\n\n    /**\n     * 卡牌生效\n     */\n    Power,\n\n    /**\n     * 交易\n     */\n    Trade\n\n}\n\ninternal fun String.toBlockType(fallback: BlockType = BlockType.Trigger): BlockType {\n\n    BlockType.values().forEach {\n        if (it.name.equals(this, true)) {\n            return it\n        }\n    }\n\n    return fallback\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/Card.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport android.os.Parcelable\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport com.squareup.moshi.JsonClass\nimport kotlinx.parcelize.Parcelize\n\n@Entity(tableName = \"card\")\n@JsonClass(generateAdapter = true)\n@Parcelize\ndata class Card(\n    /**\n     * 画家\n     */\n    val artist: String? = null,\n    /**\n     * 名称\n     */\n    val name: String,\n    /**\n     * 费用\n     */\n    val cost: Int = 0,\n    @PrimaryKey\n    val id: String,\n    val dbfId: Int,\n    val text: String = \"\",\n    //属于哪个版本 例如 TGT\n    val set: String = \"\",\n    val type: CardType = CardType.None,\n    val cardClass: CardClass? = null,\n    val classes: List<CardClass> = emptyList(),\n    val mechanics: List<Mechanics> = emptyList(),\n    /**\n     * 个性介绍\n     */\n    val flavor: String = \"\",\n\n    val rarity: Rarity? = null,\n\n    /**\n     * 武器耐久\n     */\n    val durability: Int = 0,\n    /**\n     * 英雄牌的护甲\n     */\n    val armor: Int = 0,\n\n    val collectible: Boolean = false,\n    /**\n     * 法术类型\n     */\n    val spellSchool: SpellSchool? = null,\n\n    /**\n     * 随从种族\n     */\n    val race: Race? = null,\n\n    /**\n     * 随从攻击力\n     */\n    val attack: Int = 0,\n    /**\n     * 随从生命值\n     */\n    val health: Int = 0,\n) : Parcelable\n\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/CardBean.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\ndata class CardBean(\n    val card: Card,\n    val count: Int = 0\n) {\n    //防止在使用MutableStateFlow时无法更新数据\n//    override fun equals(other: Any?): Boolean {\n//        return false\n//    }\n//\n//    override fun hashCode(): Int {\n//        return Random.nextInt()\n//    }\n\n    fun updateCount(count: Int): CardBean {\n        return CardBean(card, count)\n    }\n\n\n    fun toCardList(): List<Card> {\n        val list = mutableListOf<Card>()\n        repeat(count) {\n            list.add(card)\n        }\n        return list\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/CardClass.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport androidx.annotation.ColorRes\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport com.ke.hs_tracker.module.R\nimport com.squareup.moshi.FromJson\nimport com.squareup.moshi.ToJson\n\nenum class CardClass(\n    @StringRes\n    val titleRes: Int,\n    @ColorRes\n    val color: Int = 0,\n\n    @DrawableRes\n    val roundIcon: Int? = null,\n    val isHero: Boolean = true\n) {\n    /**\n     * 法师\n     */\n    Mage(R.string.module_mage, R.color.module_mage, R.drawable.module_image_round_mage),\n\n    /**\n     * 术士\n     */\n    Warlock(R.string.module_warlock, R.color.module_warlock, R.drawable.module_image_round_warlock),\n\n    /**\n     * 牧师\n     */\n    Priest(R.string.module_priest, R.color.module_priest, R.drawable.module_image_round_priest),\n\n    /**\n     * 德鲁伊\n     */\n    Druid(R.string.module_druid, R.color.module_druid, R.drawable.module_image_round_druid),\n\n    /**\n     * 盗贼\n     */\n    Rogue(R.string.module_rogue, R.color.module_rogue, R.drawable.module_image_round_rogue),\n\n    /**\n     * 萨满\n     */\n    Shaman(R.string.module_shaman, R.color.module_shaman, R.drawable.module_image_round_shaman),\n\n\n    /**\n     * 猎人\n     */\n    Hunter(R.string.module_hunter, R.color.module_hunter, R.drawable.module_image_round_hunter),\n\n    /**\n     * 圣骑士\n     */\n    Paladin(R.string.module_paladin, R.color.module_paladin, R.drawable.module_image_round_paladin),\n\n    /**\n     * 战士\n     */\n    Warrior(R.string.module_warrior, R.color.module_warrior, R.drawable.module_image_round_warrior),\n\n    /**\n     * 恶魔猎手\n     */\n    DemonHunter(\n        R.string.module_demon_hunter,\n        R.color.module_demon_hunter,\n        R.drawable.module_image_round_demon_hunter\n    ),\n\n    /**\n     * 中立\n     */\n    Neutral(R.string.module_neutral, R.color.module_neutral, isHero = false),\n\n    /**\n     * 威兹班\n     */\n    Whizbang(R.string.module_whizbang, 0, isHero = false),\n\n    /**\n     * 梦境牌\n     */\n    Dream(R.string.module_dream, 0, isHero = false),\n\n    /**\n     * 死亡骑士\n     */\n    DeathKnight(R.string.module_death_knight, R.color.module_death_knight, isHero = false),\n}\n\nclass CardClassAdapter {\n    @FromJson\n    fun fromJson(value: String): CardClass {\n\n        return EnumMoshiAdapter.fromJson(value, CardClass.values())\n\n    }\n\n    @ToJson\n    fun toJson(cardClass: CardClass) = cardClass.name.uppercase()\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/CardType.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport com.squareup.moshi.FromJson\nimport com.squareup.moshi.ToJson\n\n\nenum class CardType {\n\n\n    /**\n     * 英雄\n     */\n    Hero,\n\n    /**\n     * 英雄技能\n     */\n    HeroPower,\n\n\n    /**\n     *衍生牌\n     */\n    Enchantment,\n\n    /**\n     * 法术\n     */\n    Spell,\n\n    /**\n     * 随从\n     */\n    Minion,\n\n    /**\n     * 武器\n     */\n    Weapon,\n\n\n    None\n}\n\nclass CardTypeAdapter {\n\n    @FromJson\n    fun fromJson(value: String): CardType {\n\n        return CardType.values()\n            .find { it.name.equals(value.replace(\"_\", \"\"), true) } ?: CardType.None\n\n    }\n\n    @ToJson\n    fun toJson(cardType: CardType) = cardType.name.uppercase()\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/CurrentDeck.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\ndata class CurrentDeck(\n    val name: String,\n    val code: String\n)\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/Entity.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport com.ke.hs_tracker.module.parser.PowerParserImpl\n\n\n//有三种形式\n//1,Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1]\n//2,Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1]\n//3,Entity=失落的裤子#5629\ndata class Entity(\n    val entityName: String,\n    val gameCardType: GameCardType? = null,\n    val id: Int = -1,\n    val zone: Zone = Zone.Play,\n    val zonePosition: Int = -1,\n    val cardId: String? = null,\n    val player: Int = -1\n) {\n\n    val entityType: EntityType\n        get() = when {\n            gameCardType == null && id == -1 && zone == Zone.Play && zonePosition == -1 && cardId == null && player == -1 -> EntityType.Name\n            gameCardType == GameCardType.Invalid -> EntityType.Invalid\n            else -> EntityType.Clear\n        }\n\n    val isGameEntity: Boolean\n        get() = entityName == \"GameEntity\"\n\n    val isUserEntity: Boolean\n        get() = entityName.contains(\"#\")\n\n    companion object {\n        internal fun createFromContent(content: String): Entity? {\n\n            if (content == \"0\") {\n                return null\n            }\n\n\n            var matchResult = PowerParserImpl.FULL_ENTITY_CONTENT1_PATTERN.matchEntire(content)\n            if (matchResult != null) {\n                return Entity(\n                    matchResult.groupValues[1],\n                    matchResult.groupValues[2].toCardType(GameCardType.Invalid),\n                    matchResult.groupValues[3].toIntOrNull() ?: 0,\n                    matchResult.groupValues[4].toZone(),\n                    matchResult.groupValues[5].toIntOrNull() ?: 0,\n                    matchResult.groupValues[6].ifBlank { null },\n                    matchResult.groupValues[7].toIntOrNull() ?: 0\n                )\n\n            }\n            matchResult = PowerParserImpl.FULL_ENTITY_CONTENT2_PATTERN.matchEntire(content)\n                ?: return Entity(content)\n\n            return Entity(\n                matchResult.groupValues[1],\n                GameCardType.Invalid,\n                matchResult.groupValues[2].toIntOrNull() ?: 0,\n                matchResult.groupValues[3].toZone(),\n                matchResult.groupValues[4].toIntOrNull() ?: 0,\n                matchResult.groupValues[5].ifBlank { null },\n                matchResult.groupValues[6].toIntOrNull() ?: 0\n            )\n\n        }\n    }\n}\n\nenum class EntityType {\n\n    //Entity=失落的裤子#5629\n    Name,\n\n    //Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1]\n    Invalid,\n\n    //Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1]\n    Clear\n}\n\n\ndata class GameEntity(\n    val gameCardType: GameCardType,\n    val entityId: Int\n)\n\n/**\n * 玩家\n */\ndata class Player(\n    val entityId: Int,\n    val playerId: Int,\n    val controller: Int,\n    val gameCardType: GameCardType,\n    val heroEntity: Int,\n    /**\n     * 手牌上限\n     */\n    val maxHandSize: Int,\n    /**\n     * 起始手牌\n     */\n    val startHandSize: Int,\n    val teamId: Int,\n    /**\n     * 费用上限 一般为10\n     */\n    val maxResources: Int\n) {\n\n    companion object {\n        internal fun fromMap(map: Map<String, String>): Player {\n\n            return Player(\n                entityId = map[\"entityid\"]?.toIntOrNull() ?: 0,\n                playerId = map[\"playerid\"]?.toIntOrNull() ?: 0,\n                controller = map[\"controller\"]?.toIntOrNull() ?: 0,\n                gameCardType = (map[\"cardtype\"] ?: \"\").toCardType(),\n                heroEntity = map[\"heroentity\"]?.toIntOrNull() ?: 0,\n                maxHandSize = map[\"maxhandsize\"]?.toIntOrNull() ?: 0,\n                startHandSize = map[\"starthandsize\"]?.toIntOrNull() ?: 0,\n                teamId = map[\"teamid\"]?.toIntOrNull() ?: 0,\n                maxResources = map[\"maxresources\"]?.toIntOrNull() ?: 0\n            )\n\n        }\n    }\n}\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/EntityWithPayload.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\ndata class EntityWithPayload(\n    val entity: Entity,\n    private val payloads: MutableMap<String, String> = mutableMapOf()\n) {\n    fun add(pair: Pair<String, String>) {\n        payloads[pair.first] = pair.second\n    }\n\n    fun getPayloads(): Map<String, String> = payloads\n}\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/EnumMoshiAdapter.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nobject EnumMoshiAdapter {\n\n\n    fun <T : Enum<T>> fromJson(value: String, enumList: Array<T>, fallback: T? = null): T {\n        val enum = enumList\n            .find { it.name.equals(value.replace(\"_\", \"\"), true) } ?: fallback\n\n        if (enum == null) {\n            throw IllegalArgumentException(\"错误的 $value 类型\")\n        }\n\n        return enum\n\n    }\n\n    fun <T : Enum<T>> toJson(value: T) = value.name.uppercase()\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/FormatType.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport androidx.annotation.StringRes\nimport com.ke.hs_tracker.module.R\n\nenum class FormatType(@StringRes val title: Int) {\n    Unknown(R.string.module_unknown),\n\n    /**\n     * 标准\n     */\n    Standard(R.string.module_standard),\n\n    /**\n     * 狂野\n     */\n    Wild(R.string.module_wild),\n\n    /**\n     * 经典\n     */\n    Classic(R.string.module_classic)\n}\n\nval String.toFormatType: FormatType\n    get() = FormatType.values().find {\n        this.contains(it.name, true)\n    } ?: FormatType.Unknown"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/GameCardType.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\n/**\n * 卡牌类型\n */\nenum class GameCardType {\n\n    Game,\n\n    /**\n     * 玩家\n     */\n    Player,\n\n    /**\n     * 英雄\n     */\n    Hero,\n\n    /**\n     * 英雄技能\n     */\n    HeroPower,\n\n\n    /**\n     * 牌库中的牌的状态\n     */\n    Invalid,\n\n    /**\n     * 随从身上的buff或战场上的buff（例如下一张法强怪法力值减少1）\n     */\n    Enchantment,\n\n    /**\n     * 法术\n     */\n    Spell,\n\n    /**\n     * 随从\n     */\n    Minion\n}\n\n/**\n * 字符串转 CardType类型\n */\ninternal fun String.toCardType(fallback: GameCardType = GameCardType.Game): GameCardType {\n    return GameCardType.values().find {\n        it.name.equals(this.replace(\"_\", \"\"), true)\n    } ?: fallback\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/GameEvent.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport com.ke.hs_tracker.module.db.Game\n\n/**\n * 游戏事件\n */\nsealed interface GameEvent {\n\n\n\n\n    /**\n     * 游戏开始\n     */\n    object OnGameStart : GameEvent\n\n    /**\n     * 游戏结束\n     */\n    data class OnGameOver(\n        val game: Game\n    ) : GameEvent\n\n\n    /**\n     * 插入一张卡牌到用户的牌库\n     */\n    data class InsertCardToUserDeck(\n        val cardId: String\n    ) : GameEvent\n\n    /**\n     * 从牌库中移除一张卡牌\n     */\n    data class RemoveCardFromUserDeck(\n        val cardId: String\n    ) : GameEvent\n\n    /**\n     * 插入一张卡牌到墓地\n     */\n    data class InsertCardToGraveyard(val cardId: String, val isUser: Boolean) : GameEvent\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/GameType.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport androidx.annotation.StringRes\nimport com.ke.hs_tracker.module.R\n\nenum class GameType(@StringRes val title: Int) {\n    /**\n     * 排名\n     */\n    Ranked(R.string.module_ranked),\n\n    /**\n     * 休闲\n     */\n    Casual(R.string.module_casual),\n\n    Unknown(R.string.module_unknown)\n}\n\nval String.toGameType: GameType\n    get() = GameType.values().find {\n        this.contains(it.name, true)\n    } ?: GameType.Unknown"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/GraveyardCard.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\ndata class GraveyardCard(\n    val card: Card,\n    /**\n     * 插入时间\n     */\n    val time: Long = System.currentTimeMillis()\n)"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/InsertStackResult.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nsealed interface InsertStackResult {\n\n\n    /**\n     * 插入成功\n     */\n    object Success : InsertStackResult\n\n    /**\n     * 不能插入，例如不在Block内的TAG_CHANGE\n     */\n    object CanNotInsert : InsertStackResult\n\n    /**\n     * 结束了\n     * @param powerTag tag\n     * @param handled 是否处理了本次log日志，例如 FULL_ENTITY - Updating 跟着一个 FULL_ENTITY - Updating的情况，handled就是true，表示已经处理了，不需要在进行处理；如果是FULL_ENTITY - Updating\n     * 跟着一个 TAG_CHANGE，就表示没有处理，需要调用这个方法的自行处理log数据\n     */\n    data class Over(val powerTag: PowerTag, val handled: Boolean) : InsertStackResult\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/LogType.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nenum class LogType(val replace: String) {\n    PowerTaskList(\"PowerTaskList.DebugPrintPower() -\"),\n    GameState(\"GameState.DebugPrintGame() -\")\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/Mechanics.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport com.squareup.moshi.FromJson\nimport com.squareup.moshi.ToJson\n\n/**\n * 类型\n */\n\nenum class Mechanics {\n\n    /**\n     * 注能\n     */\n    Infuse,\n\n    /**\n     * 过载\n     */\n    Overload,\n\n\n    /**\n     * 战吼\n     */\n    BattleCry,\n\n    /**\n     * 光环\n     */\n    Aura,\n\n    /**\n     * 冲锋\n     */\n    Charge,\n\n    /**\n     * 嘲讽\n     */\n    Taunt,\n\n    /**\n     * 突袭\n     */\n    Rush,\n\n    /**\n     * 巨型\n     */\n    Colossal,\n\n    /**\n     * 探底\n     */\n    Dredge,\n\n    /**\n     * 潜行\n     */\n    Stealth,\n\n    /**\n     * 圣盾\n     */\n    DivineShield,\n\n    /**\n     * 法强\n     */\n    SpellPower,\n\n    /**\n     * 抽到的时候\n     */\n    TopDeck,\n\n    /**\n     * 亡语\n     */\n    DeathRattle,\n\n    /**\n     * 秘密选择\n     */\n    Counter,\n\n    InvisibleDeathRattle,\n\n    /**\n     * 变形\n     */\n    Morph,\n\n    /**\n     * 激怒\n     */\n    Enraged,\n\n    /**\n     * 50%几率攻击错误的敌人\n     */\n    Forgetful,\n\n    /**\n     * 本回合生效\n     */\n    TagOneTurnEffect,\n\n    /**\n     * 抉择\n     */\n    ChooseOne,\n\n    /**\n     * 荣誉击杀\n     */\n    HonorableKill,\n\n    /**\n     * 法力迸发\n     */\n    SpellBurst,\n\n    /**\n     * 腐蚀\n     */\n    Corrupt,\n\n    /**\n     * 暴怒\n     */\n    Frenzy,\n\n    /**\n     * 发现\n     */\n    Discover,\n\n    /**\n     * 冻结\n     */\n    Freeze,\n\n    /**\n     * 连击\n     */\n    Combo,\n\n    /**\n     * 无法攻击\n     */\n    CantAttack,\n\n    /**\n     * 触发\n     */\n    TriggerVisual,\n\n    /**\n     * 奥秘\n     */\n    Secret,\n\n    /**\n     * 任务\n     */\n    Quest,\n\n    /**\n     * 克苏恩\n     */\n    Ritual,\n\n    /**\n     * 交易\n     */\n    Tradeable,\n\n    /**\n     * 激励\n     */\n    Inspire,\n\n    /**\n     * 沉默\n     */\n    Silence,\n\n    /**\n     * 风怒\n     */\n    Windfury,\n\n    /**\n     * 回响\n     */\n    Echo,\n\n    /**\n     * 污手党\n     */\n    GrimyGoons,\n\n    /**\n     * 暗金教\n     */\n    Kabal,\n\n    /**\n     * 玉莲帮\n     */\n    JadeLotus,\n\n    /**\n     * 青玉魔像\n     */\n    JadeGolem,\n\n    /**\n     * 不可见的衍生牌\n     */\n    EnchantmentInvisible,\n\n    /**\n     * 英雄技能造成额外的伤害\n     */\n    HeroPowerDamage,\n\n    /**\n     * 回合结束时如果这张牌仍在手牌中，将其摧毁\n     */\n    Ghostly,\n\n    /**\n     * 受到法强翻倍\n     */\n    ReceivesDoubleSpellDamageBonus,\n\n    /**\n     * 零件\n     */\n    SparePart,\n\n    AutoAttack,\n\n    /**\n     * 唤尸者专属\n     */\n    DeathKnight,\n\n    /**\n     * 偶数\n     */\n    CollectionmanagerFilterManaEven,\n\n    /**\n     * 奇数\n     */\n    CollectionmanagerFilterManaOdd,\n\n    /**\n     * 不受法强影响的法术\n     */\n    ImmuneToSpellPower,\n\n    /**\n     * 无法被沉默\n     */\n    CantBeSilenced,\n\n\n    CantBeDestroyed,\n\n    /**\n     * 黑棋国王\n     */\n    CantBeFatigued,\n\n    /**\n     * 支线任务\n     */\n    SideQuest,\n\n    /**\n     * 双生法术\n     */\n    TwinSpell,\n\n    /**\n     * 剧毒\n     */\n    Poisonous,\n\n    /**\n     * 吸血\n     */\n    LifeSteal,\n\n    /**\n     * 流放\n     */\n    Outcast,\n\n    /**\n     * 不可接触的\n     */\n    Untouchable,\n\n    /**\n     * 复仇\n     */\n    Avenge,\n\n    /**\n     * 超杀\n     */\n    Overkill,\n\n    /**\n     * 复生\n     */\n    Reborn,\n\n    AIMustPlay,\n\n    /**\n     * 免疫\n     */\n    Immune,\n\n    /**\n     * 无法成为法术的目标\n     */\n    CantBeTargetedBySpells,\n\n    /**\n     * 无法成为英雄技能的目标\n     */\n    CantBeTargetedByHeroPowers,\n\n    /**\n     * 磁力\n     */\n    Modular,\n\n    /**\n     * 如果这张牌在你的手牌中，在你的回合开始时，你的英雄受到2点伤害\n     */\n    EvilGlow,\n\n    /**\n     * 相邻的随从获得buff\n     */\n    AdjacentBuff,\n\n    /**\n     * 对局开始时\n     */\n    StartOfGame,\n\n    /**\n     * 在你攻击一个随从后，迫使其攻击相邻的一个随从\n     */\n    FinishAttackSpellOnDamage,\n\n\n    AppearFunctionallyDead,\n\n    Gears,\n\n    Puzzle,\n\n    MultiplyBuffValue,\n\n    AffectedBySpellPower,\n\n\n    DungeonPassiveBuff,\n\n    IgnoreHideStatsForBigCard,\n\n    Summoned,\n\n    /**\n     * 法力渴求\n     */\n    ManaThirst,\n\n    OVERHEAL,\n\n    VENOMOUS,\n\n    MAGNETIC,\n\n    FORGE,\n\n    TITAN\n}\n\n\nclass MechanicsAdapter {\n\n    @FromJson\n    fun fromJson(value: String): Mechanics {\n\n        return EnumMoshiAdapter.fromJson(value, Mechanics.values())\n\n    }\n\n    @ToJson\n    fun toJson(mechanics: Mechanics) = EnumMoshiAdapter.toJson(mechanics)\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/NestedTag.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\ninternal sealed interface NestedTag {\n    object CreateGame : NestedTag\n    data class GameEntity(val id: Int) : NestedTag\n    data class Tag(val key: String, val value: String) : NestedTag {\n        fun toPair(): Pair<String, String> = key to value\n    }\n\n    data class Player(val entityId: Int, val playerId: Int) : NestedTag\n\n    //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] CardID=\n    //FULL_ENTITY - Updating [entityName=全副武装！ id=65 zone=PLAY zonePos=0 cardId=HERO_01bp player=1] CardID=HERO_01bp\n    data class FullEntity(\n        val entity: Entity,\n        val cardId: String?\n    ) : NestedTag\n\n    data class Block(\n        val blockType: BlockType,\n        val entity: Entity,\n        val target: Entity?\n    ) : NestedTag\n\n\n    data class TagChange(\n        val entity: Entity,\n        val tag: String,\n        val value: String\n    ) : NestedTag {\n        fun convert(): PowerTag.PowerTaskList.TagChange {\n            return PowerTag.PowerTaskList.TagChange(entity, tag, value)\n        }\n    }\n\n\n    data class ShowEntity(\n        val entity: Entity,\n        val cardId: String\n    ) : NestedTag\n\n    object BlockEnd : NestedTag\n}\n\n//internal fun NestedTag.FullEntity.toUpdating(): PowerTag.PowerTaskList.FullEntity.Updating {\n//    return PowerTag.PowerTaskList.FullEntity.Updating(\n//        entityName, cardType, id, zone, zonePosition, cardId, player\n//    )\n//}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/PowerTag.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport com.ke.hs_tracker.module.db.ZonePositionChangedEvent\n\n\nsealed interface PowerTag {\n\n\n    sealed interface PowerTaskList : PowerTag {\n\n\n        /**\n         * 创建游戏\n         */\n        //D 19:55:18.1257030 GameState.DebugPrintPower() - CREATE_GAME\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -     GameEntity EntityID=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CARDTYPE value=GAME\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ZONE value=PLAY\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ENTITY_ID value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=937 value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=SPAWN_TIME_COUNT value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=GAME_SEED value=1950487951\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -     Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CONTROLLER value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CARDTYPE value=PLAYER\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=PLAYER_ID value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=HERO_ENTITY value=64\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=MAXHANDSIZE value=10\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=STARTHANDSIZE value=4\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=TEAM_ID value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ZONE value=PLAY\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ENTITY_ID value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=MAXRESOURCES value=10\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=SPAWN_TIME_COUNT value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=AVRANK value=336\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -     Player EntityID=3 PlayerID=2 GameAccountId=[hi=144115211015832391 lo=44511141]\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CONTROLLER value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=CARDTYPE value=PLAYER\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=PLAYER_ID value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=HERO_ENTITY value=66\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=MAXHANDSIZE value=10\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=STARTHANDSIZE value=4\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=TEAM_ID value=2\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ZONE value=PLAY\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=ENTITY_ID value=3\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=MAXRESOURCES value=10\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=SPAWN_TIME_COUNT value=1\n        //D 19:55:18.1257030 GameState.DebugPrintPower() -         tag=AVRANK value=338\n        data class CreateGame(\n            val gameEntity: GameEntity,\n            val player1: Player,\n            val player2: Player\n        ) : PowerTaskList\n\n\n        data class TagChange(\n            override val entity: Entity,\n            val tag: String,\n            val value: String,\n        ) : PowerTaskList, ZoneUpdatable {\n\n            /**\n             * 是否是游戏完成的标志\n             */\n            val isGameComplete: Boolean =\n                entity.entityName == \"GameEntity\" && tag.equals(\"state\", true) && value.equals(\n                    \"COMPLETE\",\n                    true\n                )\n\n            /**\n             * 是否是玩家胜利或失败\n             */\n            val isPlayerWonOrLost: Pair<String, Boolean>?\n                get() {\n                    return if (tag == \"PLAYSTATE\" && value == \"WON\") {\n                        entity.entityName to true\n                    } else if (tag == \"PLAYSTATE\" && value == \"LOST\") {\n                        entity.entityName to false\n                    } else {\n                        null\n                    }\n                }\n\n            //TAG_CHANGE Entity=GameEntity tag=TURN value=1\n            fun isTurnChanged(): Int? {\n                if (entity.isGameEntity && tag == \"TURN\") {\n                    return value.toIntOrNull()\n                }\n                return null\n            }\n\n            //TAG_CHANGE Entity=GameEntity tag=NUM_TURNS_IN_PLAY value=5\n            fun isNumTurnsInPlayChanged(): Int? {\n                if (entity.isGameEntity && tag == \"NUM_TURNS_IN_PLAY\") {\n                    return value.toIntOrNull()\n                }\n                return null\n            }\n\n            override fun getZoneString(): String? {\n                return if (tag == \"ZONE\") value else null\n            }\n\n            override fun getZonePositionString(): String? {\n                return if (tag == \"ZONE_POSITION\") value else null\n            }\n        }\n\n\n        data class FullEntity(\n            override val entity: Entity,\n            val payloads: MutableMap<String, String> = mutableMapOf(),\n            val cardId: String?\n        ) : PowerTaskList, ZoneUpdatable {\n            fun append(value: Pair<String, String>) {\n                payloads[value.first] = value.second\n            }\n\n            /**\n             * 是否是更新英雄置于战场\n             */\n            fun isUpdateHero(): Pair<Int, String?>? {\n                if (entity.zone == Zone.Play && payloads.count {\n                        it.key == \"CARDTYPE\" && it.value == \"HERO\"\n                    } == 1) {\n                    return entity.player to entity.cardId\n                }\n\n                return null\n            }\n\n            //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=50 zone=DECK zonePos=0 cardId= player=2] CardID=\n            //    tag=ZONE value=DECK\n            //    tag=CONTROLLER value=2\n            //    tag=ENTITY_ID value=50\n\n\n            override fun getZoneString(): String? {\n                return payloads[\"ZONE\"]\n            }\n\n            override fun getZonePositionString(): String? {\n                return payloads[\"ZONE_POSITION\"]\n            }\n\n            override fun convertEntity(entity: Entity): Entity {\n                entity.run {\n                    return Entity(\n                        entityName,\n                        gameCardType,\n                        id,\n                        zone,\n                        zonePosition,\n                        this@FullEntity.cardId,\n                        player\n                    )\n                }\n            }\n\n            //起始发牌\n            //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=49 zone=DECK zonePos=0 cardId= player=2] CardID=\n            //    tag=ZONE value=DECK\n            //    tag=CONTROLLER value=2\n            //    tag=ENTITY_ID value=49\n            //发现一张牌\n            //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=113 zone=SETASIDE zonePos=0 cardId= player=2] CardID=\n            //    tag=ZONE value=SETASIDE\n            //    tag=CONTROLLER value=2\n            //    tag=ENTITY_ID value=113\n//            override fun shouldIgnoreSameZone(): Boolean {\n//                return true\n//            }\n        }\n\n        data class ShowEntity(\n            override val entity: Entity,\n            val cardId: String,\n            val payloads: MutableMap<String, String> = mutableMapOf()\n        ) : PowerTaskList, ZoneUpdatable {\n            fun append(value: Pair<String, String>) {\n                payloads[value.first] = value.second\n            }\n\n            fun entityWithCardId(): Entity {\n                return entity.run {\n                    Entity(\n                        entityName,\n                        gameCardType,\n                        id,\n                        zone,\n                        zonePosition,\n                        this@ShowEntity.cardId,\n                        player\n                    )\n                }\n            }\n\n            override fun getZoneString(): String? {\n                return payloads[\"ZONE\"]\n            }\n\n            override fun getZonePositionString(): String? {\n                return payloads[\"ZONE_POSITION\"]\n            }\n\n            override fun convertEntity(entity: Entity): Entity {\n                entity.run {\n                    return Entity(\n                        entityName,\n                        gameCardType,\n                        id,\n                        zone,\n                        zonePosition,\n                        this@ShowEntity.cardId,\n                        player\n                    )\n                }\n            }\n\n\n//            override fun isUpdated(): Pair<Entity, Zone>? {\n//                val newZoneString = payloads[\"ZONE\"] ?: return null\n//                val newZone = newZoneString.toZone(Zone.Unknown)\n//                if (newZone == Zone.Unknown) {\n//                    throw RuntimeException(\"错误的zone $newZoneString\")\n//                }\n//                if (entity.zone == newZone) {\n//                    return null\n//                }\n//                return entity to newZone\n//            }\n        }\n\n        data class Block(\n            val type: BlockType,\n            val entity: Entity,\n            val target: Entity?,\n            val list: List<PowerTag>\n        ) : PowerTaskList {\n\n            /**\n             * 是否是第一回合\n             */\n            fun ifFirstTurn(): Boolean {\n                return type == BlockType.Trigger && entity.isGameEntity && list.mapNotNull {\n                    it as? TagChange\n                }.any {\n                    it.tag == \"FIRST_PLAYER\" && it.value == \"1\"\n                }\n\n            }\n\n            private fun isTurnChanged(): Int? {\n                return list.mapNotNull {\n                    it as? TagChange\n                }.find {\n                    it.isTurnChanged() != null\n                }?.isTurnChanged()\n            }\n\n            private fun isNumTurnsInPlayChanged(): Int? {\n                return list.mapNotNull {\n                    it as? TagChange\n                }.find {\n                    it.isNumTurnsInPlayChanged() != null\n                }?.isNumTurnsInPlayChanged()\n            }\n\n\n            private fun flush(): List<EntityWithPayload> {\n                val entityWithPayloadList = mutableListOf<EntityWithPayload>()\n\n                list.forEach {\n                    when (it) {\n\n                        is ShowEntity -> {\n                            val entity = it.entityWithCardId()\n                            val entityWithPayload = EntityWithPayload(entity)\n//                            entityWithPayload.payload.addAll(it.payloads)\n                            it.payloads.forEach { entry ->\n                                entityWithPayload.add(entry.toPair())\n                            }\n                            entityWithPayloadList.add(entityWithPayload)\n\n                        }\n                        is TagChange -> {\n                            findEntityFromList(it.entity.id, entityWithPayloadList)?.apply {\n                                add(it.tag to it.value)\n                            }\n                        }\n                        else -> {\n\n                        }\n                    }\n                }\n\n                return entityWithPayloadList\n            }\n\n            private fun findEntityFromList(\n                entityId: Int,\n                list: List<EntityWithPayload>\n            ): EntityWithPayload? {\n                return list.find {\n                    it.entity.id == entityId\n                }\n            }\n\n\n        }\n    }\n\n    sealed interface GameState : PowerTag {\n        data class BuildNumber(val number: String) : GameState\n\n        data class GameType(val type: String) : GameState\n\n        data class FormatType(val type: String) : GameState\n\n        data class ScenarioID(val id: String) : GameState\n\n        data class PlayerMapping(val id: Int, val name: String) : GameState {\n\n            /**\n             * 是否是先手\n             */\n            val first: Boolean = id == 1\n\n            /**\n             * 是否是当前用户\n             */\n            val isUser: Boolean = name != \"UNKNOWN HUMAN PLAYER\"\n        }\n    }\n\n\n}\n\ninterface ZoneUpdatable {\n    /**\n     * 是否更新了位置\n     */\n    fun isUpdateZone(userPlayerId: Int?): ZonePositionChangedEvent? {\n        val newZoneString = getZoneString()\n        val newZone = newZoneString?.toZone(Zone.Unknown)\n        if (newZone == Zone.Unknown) {\n            throw RuntimeException(\"错误的zone $newZoneString\")\n        }\n\n        val position = getZonePositionString()?.toIntOrNull()\n\n        if (newZone == null && position == null) {\n            return null\n        }\n\n        val entity = convertEntity(entity)\n\n        return ZonePositionChangedEvent(\n            entityId = entity.id,\n            cardId = entity.cardId,\n            currentZone = entity.zone,\n            isUser = entity.player == userPlayerId,\n            currentPosition = entity.zonePosition,\n            newZone = newZone ?: entity.zone,\n            newPosition = position ?: entity.zonePosition\n        )\n    }\n\n    fun getZoneString(): String?\n\n    fun getZonePositionString(): String?\n\n    val entity: Entity\n\n\n    fun convertEntity(entity: Entity): Entity = entity\n\n\n}\n\n//interface ZonePositionUpdatable {\n//    /**\n//     * 是否更新了位置\n//     */\n//    fun isUpdatePosition(): Pair<Entity, Int>? {\n//\n//        val position = getZonePositionString()?.toIntOrNull() ?: return null\n//\n//        return entity to position\n//    }\n//\n//    fun getZonePositionString(): String?\n//\n//    val entity: Entity\n//\n//\n//}\n\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/Race.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport androidx.annotation.StringRes\nimport com.ke.hs_tracker.module.R\nimport com.squareup.moshi.FromJson\nimport com.squareup.moshi.ToJson\n\n/**\n * 随从种族\n */\nenum class Race(@StringRes val titleRes: Int? = null, val tradition: Boolean = false) {\n    /**\n     * 海盗\n     */\n    Pirate(R.string.module_pirate, true),\n\n    /**\n     * 机械\n     */\n    Mechanical(R.string.module_mechanical, true),\n\n    /**\n     * 龙\n     */\n    Dragon(R.string.module_dragon, true),\n\n    /**\n     * 野兽\n     */\n    Beast(R.string.module_beast, true),\n\n    /**\n     * 鱼人\n     */\n    Murloc(R.string.module_murloc, true),\n\n    /**\n     * 恶魔\n     */\n    Demon(R.string.module_demon, true),\n\n    /**\n     * 图腾\n     */\n    Totem(R.string.module_totem, true),\n\n    /**\n     * 元素\n     */\n    Elemental(R.string.module_elemental, true),\n\n\n    /**\n     * 娜迦\n     */\n    Naga(R.string.module_naga, true),\n\n\n    Error,\n\n\n    /**\n     * 野猪人\n     */\n    Quilboar(R.string.module_quilboar),\n\n    /**\n     * 全部\n     */\n    All(R.string.module_all),\n\n    /**\n     * 佣兵\n     */\n    ORC,\n\n    Troll,\n\n    /**\n     * 暗夜精灵\n     */\n    NightElf,\n\n    /**\n     * 食人魔\n     */\n    Ogre,\n\n    /**\n     * 牛头人\n     */\n    Tauren,\n\n    Lock,\n\n    Human,\n\n    Bloodelf,\n\n    OldGod,\n\n    /**\n     * 亡灵\n     */\n    Undead,\n\n    Gnome,\n\n    Dwarf,\n\n    /**\n     * 德莱尼\n     */\n    Draenei,\n\n    Halforc,\n\n    Centaur,\n\n\n    Goblin,\n\n    Furbolg,\n\n    Egg,\n\n    Worgen,\n\n    Treant,\n\n    Highelf,\n}\n\nclass RaceAdapter {\n\n\n    @FromJson\n    fun fromJson(value: String): Race {\n        return EnumMoshiAdapter.fromJson(value, Race.values(), Race.Error)\n    }\n\n    @ToJson\n    fun toJson(value: Race) = EnumMoshiAdapter.toJson(value)\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/Rarity.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport androidx.annotation.ColorRes\nimport androidx.annotation.StringRes\nimport com.ke.hs_tracker.module.R\nimport com.squareup.moshi.FromJson\nimport com.squareup.moshi.ToJson\n\nenum class Rarity(@StringRes val titleRes: Int, @ColorRes val colorRes: Int) {\n\n    /**\n     * 免费\n     */\n    Free(R.string.module_rarity_free, R.color.module_rarity_common),\n\n    /**\n     * 普通\n     */\n    Common(R.string.module_rarity_common, R.color.module_rarity_common),\n\n    /**\n     * 稀有\n     */\n    Rare(R.string.module_rarity_rare, R.color.module_rarity_rare),\n\n    /**\n     * 史诗\n     */\n    Epic(R.string.module_rarity_epic, R.color.module_rarity_epic),\n\n    /**\n     * 传说\n     */\n    Legendary(R.string.module_rarity_legendary, R.color.module_rarity_legendary)\n}\n\nclass RarityAdapter {\n\n\n    @FromJson\n    fun fromJson(value: String): Rarity {\n        return EnumMoshiAdapter.fromJson(\n            value, Rarity.values()\n        )\n    }\n\n    @ToJson\n    fun toJson(value: Rarity) = EnumMoshiAdapter.toJson(value)\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/SpellSchool.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport androidx.annotation.StringRes\nimport com.ke.hs_tracker.module.R\nimport com.squareup.moshi.FromJson\nimport com.squareup.moshi.ToJson\n\n/**\n * 法术类型\n */\nenum class SpellSchool(@StringRes val titleRes: Int) {\n    /**\n     * 奥数\n     */\n    Arcane(R.string.module_arcane),\n\n//    /**\n//     * 冰霜\n//     */\n//    Freeze,\n\n    /**\n     * 冰霜\n     */\n    Frost(R.string.module_frost),\n\n    /**\n     * 火焰\n     */\n    Fire(R.string.module_fire),\n\n    /**\n     * 自然\n     */\n    Nature(R.string.module_nature),\n\n    /**\n     * 暗影\n     */\n    Shadow(R.string.module_shadow),\n\n    /**\n     * 神圣\n     */\n    Holy(R.string.module_holy),\n\n    /**\n     * 邪能\n     */\n    Fel(R.string.module_Fel),\n\n    /**\n     * 冲锋攻击\n     */\n    PhysicalCombat(R.string.module_all)\n}\n\nclass SpellSchoolAdapter {\n    @FromJson\n    fun fromJson(value: String): SpellSchool? {\n\n        return SpellSchool.values().find { it.name.equals(value.replace(\"_\", \"\"), true) }\n\n    }\n\n    @ToJson\n    fun toJson(spellSchool: SpellSchool?) = spellSchool?.name?.uppercase()\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/Turn.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nsealed interface Turn {\n\n\n    /**\n     * 游戏初始化\n     */\n    data class InitialGame(\n        val buildNumber: String,\n        val gameType: String,\n        val formatType: String,\n        val scenarioID: Int,\n        val player1: Pair<Int, String>,\n        val player2: Pair<Int, String>,\n    )\n\n    /**\n     * 卡牌初始化\n     */\n    data class CreateGame(\n        val player1Cards: List<Int>,\n        val player2Cards: List<Int>,\n        val player1HeroId: String,\n        val player1HeroPowerId: String,\n        val player2HeroId: String,\n        val player2HeroPowerId: String\n    ) : Turn\n\n\n    /**\n     * 确定手牌回合\n     */\n    data class First(\n        val firstPlayerName: String,\n        val player1Cards: List<PlayerInitialCard>,\n        val player2Cards: List<PlayerInitialCard>\n    ) : Turn\n}\n\ndata class PlayerInitialCard(\n    val playerId: Int,\n    val position: Int,\n    val entityId: Int,\n    //自己的卡牌才能看到id\n    val cardId: String?\n)"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/Zone.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\n\nenum class Zone {\n    /**\n     * 战场\n     */\n    Play,\n\n    /**\n     * 牌库\n     */\n    Deck,\n\n    /**\n     * 发现的牌的位置\n     */\n    SetAside,\n\n    /**\n     * 墓地 打出的法术牌和死亡的随从会进入\n     */\n    Graveyard,\n\n    /**\n     * 手牌\n     */\n    Hand,\n\n    /**\n     * 位置\n     */\n    Unknown,\n\n    /**\n     * 衍生牌被消耗后会去到这个地方\n     */\n    //TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1068 value=5\n    //TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1068 value=0\n    //TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=ZONE value=REMOVEDFROMGAME\n    //TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1234 value=94\n    RemovedFromGame,\n\n    /**\n     * 奥秘\n     */\n    Secret\n}\n\ninternal fun String.toZone(fallback: Zone = Zone.Deck): Zone {\n    return Zone.values().firstOrNull {\n        it.name.equals(this, true)\n    } ?: fallback\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/entity/ZoneCard.kt",
    "content": "package com.ke.hs_tracker.module.entity\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class ZoneCard(\n    val card: Card?,\n    val entityId: Int,\n    val position: Int\n) : Parcelable"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/parser/BlockTagStack.kt",
    "content": "package com.ke.hs_tracker.module.parser\n\nimport com.ke.hs_tracker.module.entity.*\nimport com.orhanobut.logger.Logger\nimport java.util.*\nimport javax.inject.Inject\n\n\ninterface BlockTagStack {\n\n    /**\n     * 插入一条日志\n     */\n    fun insert(line: String): InsertStackResult\n\n\n}\n\nclass BlockTagStackImpl @Inject constructor() : BlockTagStack {\n\n    private val nestedTagList = LinkedList<NestedTag>()\n\n\n    override fun insert(line: String): InsertStackResult {\n\n        //CREATE_GAME\n        var matchResult = PowerParserImpl.CREATE_GAME_PATTERN.matchEntire(line)\n\n        if (matchResult != null) {\n            //游戏开始\n            nestedTagList.clear()\n            nestedTagList.add(NestedTag.CreateGame)\n            return InsertStackResult.Success\n        }\n\n        //GameEntity EntityID=1\n        matchResult = PowerParserImpl.GAME_ENTITY_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val entityId = matchResult.groupValues[1].toInt()\n\n            nestedTagList.add(NestedTag.GameEntity(entityId))\n            return InsertStackResult.Success\n        }\n        //Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]\n        matchResult = PowerParserImpl.PLAYER_ENTITY_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val entityId = matchResult.groupValues[1].toInt()\n            val playerId = matchResult.groupValues[2].toInt()\n            nestedTagList.add(NestedTag.Player(entityId, playerId))\n            return InsertStackResult.Success\n        }\n        //tag=CARDTYPE value=GAME\n        matchResult = PowerParserImpl.TAG_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val key = matchResult.groupValues[1]\n            val value = matchResult.groupValues[2]\n            nestedTagList.add(NestedTag.Tag(key, value))\n            return InsertStackResult.Success\n        }\n\n        //BLOCK_START\n        // BlockType=TRIGGER\n        // Entity=GameEntity\n        // EffectCardId=System.Collections.Generic.List`1[System.String]\n        // EffectIndex=-1\n        // Target=0\n        // SubOption=-1\n        // TriggerKeyword=TAG_NOT_SET\n        matchResult = PowerParserImpl.BLOCK_START_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val blockType = matchResult.groupValues[1].toBlockType()\n            val entity = Entity.createFromContent(matchResult.groupValues[2])!!\n            val target = Entity.createFromContent(matchResult.groupValues[5])\n            val block = NestedTag.Block(blockType, entity, target)\n            nestedTagList.add(block)\n            return InsertStackResult.Success\n        }\n\n        matchResult = PowerParserImpl.BLOCK_END_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            //块结束了\n            if (nestedTagList.isEmpty()) {\n                Logger.d(\"准备插入一个end到空的列表里面\")\n                return InsertStackResult.Success\n            }\n\n            nestedTagList.add(NestedTag.BlockEnd)\n//            val blockStartList = nestedTagList.filterIsInstance<NestedTag.Block>()\n            val blockCount = nestedTagList.count {\n                it is NestedTag.Block\n            }\n            val blockEndCount = nestedTagList.count { it is NestedTag.BlockEnd }\n\n            if (blockCount == blockEndCount) {\n                return InsertStackResult.Over(flushBlock(nestedTagList), true)\n            } else {\n                return InsertStackResult.Success\n            }\n\n        }\n\n        //TAG_CHANGE Entity=阿克萌德#51240 tag=CURRENT_PLAYER value=1\n        //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=1\n        matchResult = PowerParserImpl.TAG_CHANGE_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            return when (val first = nestedTagList.firstOrNull()) {\n\n                is NestedTag.FullEntity -> {\n                    val powerTag = flushFullEntityWhenFirst()\n                    //处理堆栈\n                    InsertStackResult.Over(powerTag, false)\n                }\n                is NestedTag.ShowEntity -> {\n\n                    val showEntity = PowerTag.PowerTaskList.ShowEntity(\n                        first.entity,\n                        first.cardId\n                    )\n                    nestedTagList.forEach {\n                        if (it is NestedTag.TagChange) {\n                            showEntity.payloads[it.tag] = it.value\n                        }\n                    }\n                    nestedTagList.clear()\n                    InsertStackResult.Over(showEntity, false)\n                }\n                is NestedTag.Block -> {\n                    val tagChange = NestedTag.TagChange(\n                        Entity.createFromContent(matchResult.groupValues[1])!!,\n                        matchResult.groupValues[2],\n                        matchResult.groupValues[3],\n                    )\n                    nestedTagList.add(tagChange)\n                    InsertStackResult.Success\n                }\n                else -> {\n                    InsertStackResult.CanNotInsert\n                }\n            }\n        }\n\n        //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1] CardID=\n        matchResult = PowerParserImpl.FULL_ENTITY_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            val first = nestedTagList.firstOrNull()\n            if (first is NestedTag.CreateGame) {\n                //create game 接 full entity\n                val createGame = createCreateGameTag()\n                val pair = matchResult.groupValues[1] to matchResult.groupValues[2]\n                insertFullEntity(pair)\n                return InsertStackResult.Over(createGame, true)\n\n            } else if (first is NestedTag.FullEntity) {\n                //连续两个full entity\n                val result = flushFullEntityWhenFirst()\n                insertFullEntity(matchResult.groupValues[1] to matchResult.groupValues[2])\n                return InsertStackResult.Over(result, true)\n            }\n            insertFullEntity(matchResult.groupValues[1] to matchResult.groupValues[2])\n            return InsertStackResult.Success\n        }\n\n        //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=85 zone=SETASIDE zonePos=0 cardId= player=2] CardID=SCH_231e\n        matchResult = PowerParserImpl.SHOW_ENTITY.matchEntire(line)\n        if (matchResult != null) {\n            val first = nestedTagList.firstOrNull()\n            val fullEntity = if (first is NestedTag.FullEntity) {\n                flushFullEntityWhenFirst()\n            } else null\n            val entity = Entity.createFromContent(matchResult.groupValues[1])!!\n            val cardId = matchResult.groupValues[2]\n            nestedTagList.add(NestedTag.ShowEntity(entity, cardId))\n            return if (fullEntity == null) {\n                InsertStackResult.Success\n            } else {\n                InsertStackResult.Over(fullEntity, true)\n            }\n\n\n        }\n\n        return InsertStackResult.CanNotInsert\n    }\n\n\n    /**\n     * 如果栈中的第一个是FullEntity，就开始处理\n     */\n    private fun flushFullEntityWhenFirst(): PowerTag.PowerTaskList.FullEntity {\n\n\n        val map = mutableMapOf<String, String>()\n        val first = nestedTagList.removeFirst() as NestedTag.FullEntity\n\n\n\n        nestedTagList.map {\n            val result =\n                it as? NestedTag.Tag ?: throw IllegalArgumentException(\"错误的类型，应该是Tag 但现在是 $it\")\n            result\n        }.forEach {\n            map[it.key] = it.value\n        }\n\n        //清空栈\n        nestedTagList.clear()\n\n        return PowerTag.PowerTaskList.FullEntity(\n            first.entity,\n            map,\n            first.cardId\n        )\n//        return when {\n//            isInsertCardToDeck(first) -> {\n//                //插入一张卡牌到牌库\n//                flushFullEntityInsertCardToDeck()\n//            }\n//            isInsertHeroToPlay(first) -> {\n//                //放置英雄牌到战场\n//                flushFullEntityInsertHeroToPlay()\n//            }\n//            isInsertHeroPowerToPlay(first) -> {\n//                //放置英雄技能到战场\n//                flushFullEntityInsertHeroPowerToPlay()\n//            }\n//            else -> throw RuntimeException(\"无法处理的 full entity $first\")\n//        }\n    }\n\n//    /**\n//     * 是否是置入英雄技能到战场\n//     */\n//    private fun isInsertHeroPowerToPlay(fullEntity: NestedTag.FullEntity): Boolean {\n//        if (fullEntity.entity.zone != Zone.Play) {\n//            return false\n//        }\n//\n//        nestedTagList.forEach {\n//            if (it is NestedTag.Tag && \"cardType\".equals(\n//                    it.key,\n//                    true\n//                ) && GameCardType.HeroPower.name.equals(\n//                    it.value.replace(\"_\", \"\"),\n//                    true\n//                )\n//            ) {\n//                return true\n//            }\n//        }\n//\n//\n//        return false\n//    }\n\n\n//    private fun flushFullEntityInsertHeroPowerToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay {\n//        var last = nestedTagList.removeLastOrNull()\n//        //移除第一个\n//        val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity\n//        val map = mutableMapOf<String, String>()\n//        while (last != null) {\n//            when (last) {\n//                is NestedTag.Tag -> {\n//                    map[last.key] = last.value\n//                }\n//                else -> {\n//                    throw RuntimeException(\"last的类型必须是Tag 但现在是 $last\")\n//                }\n//            }\n//            last = nestedTagList.removeLastOrNull()\n//        }\n//        return PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay.createFromEntityAndMap(\n//            entity,\n//            map\n//        )\n//    }\n//\n//    private fun flushFullEntityInsertHeroToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay {\n//        var last = nestedTagList.removeLastOrNull()\n//        //移除第一个\n//        val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity\n//        val map = mutableMapOf<String, String>()\n//        while (last != null) {\n//            when (last) {\n//                is NestedTag.Tag -> {\n//                    map[last.key] = last.value\n//                }\n//                else -> {\n//                    throw RuntimeException(\"last的类型必须是Tag 但现在是 $last\")\n//                }\n//            }\n//            last = nestedTagList.removeLastOrNull()\n//        }\n//        return PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay.createFromEntityAndMap(\n//            entity,\n//            map\n//        )\n//    }\n\n//    /**\n//     * 是否是置入英雄卡到战场\n//     */\n//    private fun isInsertHeroToPlay(fullEntity: NestedTag.FullEntity): Boolean {\n//        if (fullEntity.entity.zone != Zone.Play) {\n//            return false\n//        }\n//\n//        nestedTagList.forEach {\n//            if (it is NestedTag.Tag && \"cardType\".equals(\n//                    it.key,\n//                    true\n//                ) && GameCardType.Hero.name.equals(\n//                    it.value,\n//                    true\n//                )\n//            ) {\n//                return true\n//            }\n//        }\n//\n//\n//        return false\n//    }\n\n//    /**\n//     * 是否是置入英雄卡到战场\n//     */\n//    private fun isInsertCardToDeck(fullEntity: NestedTag.FullEntity): Boolean {\n//\n//        return fullEntity.entity.zone == Zone.Deck && fullEntity.entity.gameCardType == GameCardType.Invalid\n//    }\n\n//    private fun flushFullEntityInsertCardToDeck(): PowerTag.PowerTaskList.FullEntity.InsertToDeck {\n//        var last = nestedTagList.removeLastOrNull()\n//        //移除第一个\n//        val fullEntity = nestedTagList.removeFirst() as NestedTag.FullEntity\n//        val map = mutableMapOf<String, String>()\n//        while (last != null) {\n//            when (last) {\n//                is NestedTag.Tag -> {\n//                    map[last.key] = last.value\n//                }\n//                else -> {\n//                    throw RuntimeException(\"last的类型必须是Tag 但现在是 $last\")\n//                }\n//            }\n//            last = nestedTagList.removeLastOrNull()\n//        }\n//\n//        if (map.size != 3) throw RuntimeException(\"在插入卡牌到牌库的情况下，tag数量必须是3个\")\n//\n//        return PowerTag.PowerTaskList.FullEntity.InsertToDeck.createFromEntityAndMap(\n//            fullEntity.entity,\n//            map\n//        )\n//\n//    }\n\n\n    private fun insertFullEntity(pair: Pair<String, String>) {\n        val fullEntity =\n            createFullEntityByContent(pair.first, pair.second.ifEmpty { null })\n        nestedTagList.add(fullEntity)\n    }\n\n    /**\n     * 根据字符串创建FullEntity\n     */\n    //[entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1]\n    //[entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1]\n    private fun createFullEntityByContent(content: String, cardId: String?): NestedTag.FullEntity {\n        val entity: Entity = Entity.createFromContent(content)!!\n        return NestedTag.FullEntity(entity, cardId)\n    }\n\n\n    private fun createCreateGameTag(): PowerTag.PowerTaskList.CreateGame {\n        val first = nestedTagList.removeFirstOrNull()\n\n        if (first == NestedTag.CreateGame) {\n            val keyValueMap = mutableMapOf<String, String>()\n\n            var last = nestedTagList.removeLastOrNull()\n            var player1: Player? = null\n            var player2: Player? = null\n            while (last != null) {\n\n                when (last) {\n\n                    is NestedTag.GameEntity -> {\n                        val game = GameEntity(\n                            GameCardType.Game,\n                            last.id\n                        )\n                        return PowerTag.PowerTaskList.CreateGame(\n                            game,\n                            player1!!,\n                            player2!!\n                        )\n                    }\n                    is NestedTag.Player -> {\n                        keyValueMap[\"playerid\"] = last.playerId.toString()\n                        keyValueMap[\"entityid\"] = last.entityId.toString()\n                        if (player2 == null) {\n                            player2 = Player.fromMap(keyValueMap)\n                        } else {\n                            player1 = Player.fromMap(keyValueMap)\n                        }\n                        keyValueMap.clear()\n                    }\n                    is NestedTag.Tag -> {\n                        keyValueMap[last.key.replace(\"_\", \"\").lowercase()] = last.value\n                    }\n                    else -> throw IllegalArgumentException(\"非法状态错误 $last\")\n                }\n\n                last = nestedTagList.removeLastOrNull()\n            }\n        } else {\n            throw IllegalArgumentException(\"第一个必须是 CreateGame，但现在是 $first\")\n        }\n\n        throw RuntimeException(\"无法创建CreateGame\")\n\n    }\n\n\n    private fun flushBlock(\n        source: MutableList<NestedTag>,\n    ): PowerTag.PowerTaskList.Block {\n        //有可能出现多级嵌套\n        val header = source.removeFirst()\n        val first = header as? NestedTag.Block\n\n        if (first == null) {\n\n\n            throw RuntimeException(\"列表的第一个应该是Block，但现在是不是 现在是 $header,当前列表为 $source\")\n        }\n        source.removeLast()\n\n        val payloads = mutableListOf<PowerTag>()\n\n        val tempList = mutableListOf<NestedTag>()\n\n        source.forEachIndexed { index, nestedTag ->\n            when (nestedTag) {\n                is NestedTag.Block -> {\n//                    if (tempList.isEmpty()) {\n                    tempList.add(nestedTag)\n//                    } else {\n//                        val blockStartCount = tempList.count {\n//                            it is NestedTag.Block\n//                        }\n//                        if (blockStartCount != 1) {\n//                            throw IllegalArgumentException(\"错误的block数量 $blockStartCount\")\n//                        }\n//\n//                        val pairedBlockEndIndex = findPairBlockEndIndex(source, index)\n//                        if (pairedBlockEndIndex == -1) {\n//                            throw IllegalArgumentException(\"找不到配对的结束标识\")\n//                        }\n//                        val innerBlockList = source.subList(index, pairedBlockEndIndex)\n//                        tempList.add(flushBlock(innerBlockList))\n//                    }\n                }\n\n                is NestedTag.FullEntity -> {\n                    if (tempList.isNotEmpty()) {\n                        tempList.add(nestedTag)\n                    } else {\n                        payloads.add(\n                            PowerTag.PowerTaskList.FullEntity(\n                                nestedTag.entity,\n                                mutableMapOf(),\n                                nestedTag.cardId\n                            )\n                        )\n                    }\n                }\n\n                is NestedTag.ShowEntity -> {\n                    if (tempList.isNotEmpty()) {\n                        tempList.add(nestedTag)\n                    } else {\n                        payloads.add(\n                            PowerTag.PowerTaskList.ShowEntity(\n                                nestedTag.entity, nestedTag.cardId\n                            )\n                        )\n                    }\n                }\n                is NestedTag.Tag -> {\n\n\n                    if (tempList.isNotEmpty()) {\n                        tempList.add(nestedTag)\n                    } else {\n                        val last = payloads.last()\n                        if (last is PowerTag.PowerTaskList.FullEntity) {\n                            last.append(nestedTag.toPair())\n                        } else if (last is PowerTag.PowerTaskList.ShowEntity) {\n                            last.append(nestedTag.toPair())\n                        }\n                    }\n                }\n\n\n                is NestedTag.TagChange -> {\n                    if (tempList.isNotEmpty()) {\n                        tempList.add(nestedTag)\n                    } else {\n                        payloads.add(nestedTag.convert())\n                    }\n                }\n                NestedTag.BlockEnd -> {\n                    tempList.add(NestedTag.BlockEnd)\n\n                    val blockStartCount = tempList.count {\n                        it is NestedTag.Block\n                    }\n                    val blockEndCount = tempList.count {\n                        it is NestedTag.BlockEnd\n                    }\n                    if (blockStartCount == blockEndCount) {\n                        payloads.add(flushBlock(tempList))\n                        tempList.clear()\n                    }\n                }\n                else -> {\n                    throw IllegalArgumentException(\"非法的数据 $nestedTag\")\n                }\n            }\n\n        }\n        source.clear()\n\n        return PowerTag.PowerTaskList.Block(\n            first.blockType,\n            first.entity,\n            first.target,\n            payloads\n        )\n    }\n}\n\nprivate fun findPairBlockEndIndex(source: List<NestedTag>, start: Int): Int {\n    var blockStartCount = 0\n    source.subList(start, source.size).forEachIndexed { index, it ->\n        if (it is NestedTag.Block) {\n            blockStartCount++\n        } else if (it is NestedTag.BlockEnd) {\n            if (blockStartCount == 0) {\n                return index\n            }\n            blockStartCount--\n\n        }\n    }\n\n    return -1\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/parser/DeckCardObserver.kt",
    "content": "package com.ke.hs_tracker.module.parser\n\nimport android.content.Context\nimport androidx.documentfile.provider.DocumentFile\nimport com.ke.hs_tracker.module.db.GameDao\nimport com.ke.hs_tracker.module.domain.GetAllCardUseCase\nimport com.ke.hs_tracker.module.domain.GetRealLogDirUseCase\nimport com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase\nimport com.ke.hs_tracker.module.entity.*\nimport com.ke.hs_tracker.module.log\nimport com.ke.hs_tracker.module.ui.main.powerFileName\nimport com.ke.mvvm.base.data.successOr\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport java.io.InputStream\nimport javax.inject.Inject\n\n/**\n * 剩余卡牌监听器\n */\ninterface DeckCardObserver {\n\n    /**\n     * 牌库的卡牌\n     */\n    val deckCardList: StateFlow<List<CardBean>>\n\n    /**\n     * 自己的墓地\n     */\n    val userGraveyardCardList: StateFlow<List<CardBean>>\n\n    /**\n     * 对手的墓地\n     */\n    val opponentGraveyardCardList: StateFlow<List<CardBean>>\n\n    /**\n     * 初始化\n     */\n    fun init(scope: CoroutineScope)\n}\n\nclass DeckCardObserverImpl @Inject constructor(\n    private val powerParser: PowerParser,\n    private val powerTagHandler: PowerTagHandler,\n    private val getAllCardUseCase: GetAllCardUseCase,\n    private val parseDeckCodeUseCase: ParseDeckCodeUseCase,\n    private val getLogDirUseCase: GetRealLogDirUseCase,\n    private val gameDao: GameDao,\n    @ApplicationContext private val context: Context\n) : DeckCardObserver {\n\n\n//    private val _userGraveyardCardList = MutableStateFlow<List<GraveyardCard>>(emptyList())\n//\n//   override val userGraveyardCardList: StateFlow<List<GraveyardCard>>\n//        get() = _userGraveyardCardList\n\n    /**\n     * 当前用户的卡组\n     */\n    private var currentUserDeck: CurrentDeck? = null\n\n    private val _deckCardList =\n        MutableStateFlow<List<CardBean>>(emptyList())\n//        Channel<List<CardBean>>(capacity = Channel.CONFLATED)\n\n\n    override val deckCardList: StateFlow<List<CardBean>>\n        get() = _deckCardList\n\n    private val _userGraveyardCardList = MutableStateFlow<List<CardBean>>(emptyList())\n\n    override val userGraveyardCardList: StateFlow<List<CardBean>>\n        get() = _userGraveyardCardList\n\n    private val _opponentGraveyardCardList = MutableStateFlow<List<CardBean>>(emptyList())\n    override val opponentGraveyardCardList: StateFlow<List<CardBean>>\n        get() = _opponentGraveyardCardList\n\n    /**\n     * 所有卡牌\n     */\n    private var allCards = emptyList<Card>()\n\n    /**\n     * 当前卡组的卡牌\n     */\n    private var currentDeckList: List<CardBean> = emptyList()\n\n    /**\n     * 当前卡组剩余的卡牌\n     */\n    private var deckLeftCardList: List<CardBean> = listOf()\n\n\n    /**\n     * 获取炉石log文件夹\n     */\n    private suspend fun getLogsDir(): DocumentFile? {\n        return getLogDirUseCase(Unit).successOr(null)\n    }\n\n    /**\n     * 获取文件流\n     */\n    private suspend fun getFileStream(fileName: String): InputStream? =\n        withContext(Dispatchers.IO) {\n            val documentFile = getLogsDir()?.findFile(fileName)\n            if (documentFile == null) {\n                \"无法访问 $fileName 文件\".log()\n\n                return@withContext null\n            }\n\n            context.contentResolver.openInputStream(documentFile.uri)\n        }\n\n    override fun init(\n        scope: CoroutineScope,\n    ) {\n        val interval = 1500L\n\n        scope.launch {\n            clearPowerLogFile()\n        }\n\n        scope.launch {\n            ///获取所有卡牌\n            allCards = getAllCardUseCase(Unit).successOr(emptyList())\n        }\n\n        val deckFileObserver = DeckFileObserver(interval) {\n            getFileStream(\"Decks.log\")\n        }\n\n        val powerFileObserver = PowerFileObserver(interval) {\n            getFileStream(powerFileName)\n        }.apply {\n\n        }\n\n        scope.launch {\n            //监听牌库\n            delay(1000)\n            deckFileObserver\n                .start()\n                .flowOn(Dispatchers.IO)\n                .map {\n                    currentUserDeck = it\n                    parseDeckCodeUseCase(it.code).successOr(emptyList())\n                }.collect {\n                    currentDeckList = it\n                    _deckCardList.value = it.toList()\n//                _deckCardList.send(it)\n                }\n        }\n\n        scope.launch {\n            powerTagHandler.gameEventFlow.collect {\n                when (it) {\n                    null -> {\n\n                    }\n\n                    is GameEvent.OnGameOver -> {\n\n                        _userGraveyardCardList.value = emptyList()\n                        _opponentGraveyardCardList.value = emptyList()\n\n                        clearPowerLogFile()\n\n                        deckLeftCardList = currentDeckList.toList()\n                        _deckCardList.value = deckLeftCardList.toList()\n//                        _deckCardList.send(deckLeftCardList)\n\n\n                        it.game.apply {\n                            userDeckCode = currentUserDeck?.code ?: \"\"\n                            userDeckName = currentUserDeck?.name ?: \"\"\n                            scope.launch {\n                                gameDao.insert(this@apply)\n                            }\n                        }\n\n\n\n\n                        powerFileObserver.reset()\n                    }\n                    GameEvent.OnGameStart -> {\n\n                        _userGraveyardCardList.value = emptyList()\n                        _opponentGraveyardCardList.value = emptyList()\n//                        deckLeftCardList = currentDeckList\n//                        \"清空卡牌 OnGameStart ,deckLeftCardList ${deckLeftCardList.size} , currentDeckList ${currentDeckList.size}\".log()\n//                        deckLeftCardList.clear()\n//                        deckLeftCardList.addAll(currentDeckList)\n                        deckLeftCardList = currentDeckList.toList()\n\n                        _deckCardList.value = deckLeftCardList.toList()\n//                        _deckCardList.send(deckLeftCardList)\n                    }\n                    is GameEvent.RemoveCardFromUserDeck -> {\n                        onUserDeckCardListChanged(it.cardId, true)\n                    }\n\n                    is GameEvent.InsertCardToUserDeck -> {\n                        onUserDeckCardListChanged(it.cardId, false)\n                    }\n\n                    is GameEvent.InsertCardToGraveyard -> {\n                        onGraveyardCardsChanged(it.cardId, it.isUser)\n                    }\n                }\n            }\n        }\n\n        powerParser.powerTagListener = {\n            powerTagHandler.handle(it)\n        }\n\n\n\n        scope.launch {\n\n            powerFileObserver.start()\n                .flowOn(Dispatchers.IO)\n                .collect { list ->\n                    list.forEach {\n                        powerParser.parse(it)\n                    }\n                }\n        }\n\n    }\n\n    private fun onGraveyardCardsChanged(cardId: String, isUser: Boolean) {\n\n        //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=106 zone=PLAY zonePos=0 cardId= player=1] tag=ZONE value=GRAVEYARD\n        //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=5 zone=SECRET zonePos=0 cardId= player=1] tag=COST value=2\n        //如果对面打出一张奥秘拍 会直接进入墓地\n//            ?: throw RuntimeException(\"没有id $entity\")\n\n        val card = allCards.find {\n            it.id == cardId\n        } ?: return\n\n        if (card.type == CardType.Enchantment) {\n//            \"衍生牌 $card 不能放到墓地去\".log()\n            return\n        }\n\n//        \"插入一张牌到墓地 $card $entity\".log()\n\n        //TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD\n        if (isUser) {\n            _userGraveyardCardList.value += CardBean(card, 1)\n        } else {\n            _opponentGraveyardCardList.value += CardBean(card, 1)\n        }\n\n\n    }\n\n    /**\n     * 清空log文件\n     */\n    private suspend fun clearPowerLogFile() {\n        val documentFile = getLogsDir()?.findFile(powerFileName)\n        documentFile?.apply {\n            context.contentResolver.openOutputStream(uri, \"wt\")?.use {\n                it.write(\"\".encodeToByteArray())\n                it.flush()\n                it.close()\n            }\n        }\n    }\n\n    /**\n     * 用户牌库的卡牌发生了变化\n     */\n    private fun onUserDeckCardListChanged(cardId: String, remove: Boolean) {\n\n\n        val card = allCards.find {\n            it.id == cardId\n        } ?: throw IllegalArgumentException(\"找不到id是 $cardId 的卡牌\")\n\n\n\n        if (card.type == CardType.Enchantment) {\n            return\n        }\n\n//        \"牌库的卡牌发生了变化 $card $remove \".log()\n\n        val bean = deckLeftCardList.find {\n            it.card.id == card.id\n        }\n\n\n        val list = mutableListOf<CardBean>()\n        list.addAll(deckLeftCardList)\n\n\n        if (bean == null) {\n            list.add(CardBean(card, 1))\n        } else {\n//            bean.count =\n            val newCount = if (remove) bean.count - 1 else bean.count + 1\n            list[deckLeftCardList.indexOf(bean)] = bean.updateCount(newCount)\n        }\n\n//        if (bean?.count == 3) {\n//            \"插入了3张进去？ \".log()\n//        }\n\n        val newList = list.sortedBy {\n            it.card.cost\n        }.filter {\n            it.count > 0\n        }\n//        deckLeftCardList.clear()\n//        deckLeftCardList.addAll(newList)\n        deckLeftCardList = newList\n        _deckCardList.value = deckLeftCardList.toList()\n//        _deckCardList.send(deckLeftCardList)\n    }\n\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/parser/DeckFileObserver.kt",
    "content": "package com.ke.hs_tracker.module.parser\n\n\nimport android.os.Looper\nimport com.ke.hs_tracker.module.entity.CurrentDeck\nimport com.ke.hs_tracker.module.removeTime\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport java.io.InputStream\n\n/**\n * deck文件观察者\n */\nclass DeckFileObserver constructor(\n    private val interval: Long = 2000,\n    private val deckFileInputStreamProvider: suspend () -> InputStream?,\n) {\n    private var oldLogSize = 0L\n\n\n    fun reset() {\n        oldLogSize = 0\n    }\n\n    suspend fun start(): Flow<CurrentDeck> = flow {\n\n        if (Thread.currentThread() == Looper.getMainLooper().thread) {\n            throw RuntimeException(\"不能运行在主线程\")\n        }\n\n        while (true) {\n            deckFileInputStreamProvider()?.reader()?.apply {\n                if (oldLogSize > 0) {\n                    skip(oldLogSize)\n                }\n                val text = readText()\n                oldLogSize += text.length\n                val lines = text.lines()\n                    .filter { it.isNotEmpty() }\n\n                listToDeck(lines)?.apply {\n                    emit(this)\n                }\n                close()\n            }\n            delay(interval)\n        }\n    }\n\n    private fun listToDeck(list: List<String>): CurrentDeck? {\n        if (list.isEmpty()) {\n            return null\n        }\n        val contentList = list.map {\n            it.removeTime().third\n        }\n        val name = contentList.findLast {\n            it.startsWith(\"###\", true)\n        } ?: return null\n\n        val target =\n            contentList.subList(contentList.indexOf(name), contentList.size).toMutableList()\n        target.removeFirst()\n        target.removeFirst()\n        val code = target.removeFirst()\n\n        return CurrentDeck(\n            name.replace(\"### \", \"\"), code\n        )\n    }\n}\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/parser/PowerFileObserver.kt",
    "content": "package com.ke.hs_tracker.module.parser\n\n\nimport android.os.Looper\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport java.io.InputStream\n\n/**\n * power文件观察者\n */\nclass PowerFileObserver(\n    private val interval: Long = 2000,\n    private val fileInputStreamProvider: suspend () -> InputStream?,\n) {\n    private var oldLogSize = 0L\n\n    fun reset() {\n        oldLogSize = 0\n    }\n\n\n    suspend fun start(): Flow<List<String>> = flow {\n        if (Thread.currentThread() == Looper.getMainLooper().thread) {\n            throw RuntimeException(\"不能运行在主线程\")\n        }\n\n        delay(interval)\n\n\n        while (true) {\n\n\n            fileInputStreamProvider()\n                ?.reader()\n                ?.apply {\n                    if (oldLogSize > 0) {\n                        skip(oldLogSize)\n                    }\n\n                    val text = readText()\n//                        try {\n//                        readText()\n//                    } catch (error: Throwable) {\n//                        if (BuildConfig.DEBUG) {\n//                            error.printStackTrace()\n//                        }\n//                        val size = fileInputStreamProvider()?.available() ?: 0\n//                        Logger.d(\"内存溢出了 ，文件大小 $size,当前oldLogSize = $oldLogSize\")\n//                        oldLogSize += size\n//\n//                        readLines()\n//                        \"\"\n//                    }\n//                    readLines()\n//                    readTextFromFile()\n\n                    oldLogSize += text.length\n                    val lines = text.lines()\n                        .filter {\n                            it.startsWith(\"D\", true)\n                        }\n\n\n                    emit(lines)\n\n                    close()\n                }\n            delay(interval)\n        }\n    }\n\n\n}\n\n\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/parser/PowerParser.kt",
    "content": "package com.ke.hs_tracker.module.parser\n\nimport com.ke.hs_tracker.module.entity.Entity\nimport com.ke.hs_tracker.module.entity.InsertStackResult\nimport com.ke.hs_tracker.module.entity.LogType\nimport com.ke.hs_tracker.module.entity.PowerTag\nimport javax.inject.Inject\n\n\n/**\n * 日志解析\n */\ninterface PowerParser {\n    /**\n     * 解析一行日志\n     */\n    fun parse(content: String)\n\n    /**\n     * 解析结果监听\n     */\n    var powerTagListener: ((PowerTag) -> Unit)?\n\n\n}\n\nclass PowerParserImpl @Inject constructor(\n    private val blockTagStack: BlockTagStack\n) : PowerParser {\n\n//    private val blockTagStack: BlockTagStack = BlockTagStackImpl()\n\n\n    override var powerTagListener: ((PowerTag) -> Unit)? = null\n\n\n    override fun parse(content: String) {\n\n        val pair = checkTypeAndReturnContent(content) ?: return\n\n\n\n        if (pair.first == LogType.PowerTaskList) {\n            handlePowerTaskListLog(pair.second)\n        } else {\n            handleGameStateLog(pair.second)\n        }\n\n    }\n\n\n    /**\n     * 检查日志类型并返回去掉时间和日期前缀的内容\n     */\n    private fun checkTypeAndReturnContent(content: String): Pair<LogType, String>? {\n        if (content.length < TIME_PREFIX_SIZE) {\n            return null\n        }\n        val noTimeContent = content.substring(TIME_PREFIX_SIZE)\n        if (noTimeContent.startsWith(LogType.GameState.replace)) {\n            return LogType.GameState to noTimeContent.replace(LogType.GameState.replace, \"\").trim()\n        } else if (noTimeContent.startsWith(LogType.PowerTaskList.replace)) {\n            return LogType.PowerTaskList to noTimeContent.replace(LogType.PowerTaskList.replace, \"\")\n                .trim()\n        }\n\n        return null\n    }\n\n    private fun handlePowerTaskListLog(line: String) {\n\n        when (val result = blockTagStack.insert(line)) {\n            InsertStackResult.CanNotInsert -> {\n                handleUnSupportNestedTag(line)\n            }\n            is InsertStackResult.Over -> {\n                powerTagListener?.invoke(result.powerTag)\n                if (!result.handled) {\n                    //需要自己处理\n                    handleUnSupportNestedTag(line)\n                }\n            }\n            InsertStackResult.Success -> {\n\n            }\n        }\n\n    }\n\n    private fun handleUnSupportNestedTag(line: String) {\n        var matchResult = TAG_CHANGE_PATTERN.matchEntire(line)\n        if (matchResult != null) {\n            handleTagChangeLine(\n                matchResult.groupValues[1],\n                matchResult.groupValues[2],\n                matchResult.groupValues[3]\n            )\n        }\n    }\n\n    private fun handleTagChangeLine(content: String, tag: String, value: String) {\n\n        val entity = Entity.createFromContent(content)!!\n\n        powerTagListener?.invoke(PowerTag.PowerTaskList.TagChange(entity, tag, value))\n    }\n\n    private fun handleGameStateLog(content: String) {\n\n\n        BUILD_NUMBER_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.BuildNumber(\n                groupValues[1]\n            )\n            powerTagListener?.invoke(tag)\n            return\n        }\n\n        GAME_TYPE_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.GameType(\n                groupValues[1]\n            )\n            powerTagListener?.invoke(tag)\n            return\n        }\n        FORMAT_TYPE_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.FormatType(\n                groupValues[1]\n            )\n\n            powerTagListener?.invoke(tag)\n            return\n        }\n\n        SCENARIO_ID_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.ScenarioID(\n                groupValues[1]\n            )\n            powerTagListener?.invoke(tag)\n            return\n        }\n        PLAYER_MAPPING_PATTERN.matchEntire(content)?.apply {\n            val tag = PowerTag.GameState.PlayerMapping(\n                groupValues[1].toInt(), groupValues[2]\n            )\n            powerTagListener?.invoke(tag)\n            return\n        }\n\n    }\n\n\n    companion object {\n        const val TIME_PREFIX_SIZE = 19\n\n        //CREATE_GAME\n        internal val CREATE_GAME_PATTERN = Regex(\"CREATE_GAME\")\n\n        //BuildNumber=127581\n        internal val BUILD_NUMBER_PATTERN = Regex(\"BuildNumber=(.*)\")\n\n        //GameType=GT_CASUAL\n        internal val GAME_TYPE_PATTERN = Regex(\"GameType=(.*)\")\n\n        //FormatType=FT_WILD\n        internal val FORMAT_TYPE_PATTERN = Regex(\"FormatType=(.*)\")\n\n        //ScenarioID=2\n        internal val SCENARIO_ID_PATTERN = Regex(\"ScenarioID=(.*)\")\n\n        //PlayerID=2, PlayerName=阿克萌德#51240\n        internal val PLAYER_MAPPING_PATTERN = Regex(\"PlayerID=(.*), PlayerName=(.*)\")\n\n        // tag=CARDTYPE value=GAME\n        internal val TAG_PATTERN = Regex(\"tag=(.*) value=(.*)\")\n\n        //TAG_CHANGE Entity=GameEntity tag=STATE value=RUNNING\n        internal val TAG_CHANGE_PATTERN = Regex(\"TAG_CHANGE Entity=(.*) tag=(.*) value=(.*)\")\n\n\n        //GameEntity EntityID=1\n        internal val GAME_ENTITY_PATTERN = Regex(\"GameEntity EntityID=(.*)\")\n\n        //FULL_ENTITY - Updating [entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1] CardID=HERO_01\n        internal val FULL_ENTITY_PATTERN = Regex(\"FULL_ENTITY - Updating (.*) CardID=(.*)\")\n\n        //[entityName=UNKNOWN ENTITY [cardType=INVALID] id=83 zone=DECK zonePos=0 cardId= player=2]\n        val FULL_ENTITY_CONTENT1_PATTERN =\n            Regex(\"\\\\[entityName=(.*) \\\\[cardType=(.*)] id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]\")\n\n        val FULL_ENTITY_CONTENT2_PATTERN =\n            Regex(\"\\\\[entityName=(.*) id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]\")\n\n        val SHOW_ENTITY = Regex(\"SHOW_ENTITY - Updating Entity=(.*) CardID=(.*)\")\n\n\n        //Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]\n        internal val PLAYER_ENTITY_PATTERN =\n            Regex(\"Player EntityID=(.*) PlayerID=(.*) GameAccountId=(.*)\")\n\n        //BLOCK_START BlockType=ATTACK Entity=[entityName=瓦丝琪女士 id=87 zone=PLAY zonePos=1 cardId=BT_109 player=2]\n        // EffectCardId=System.Collections.Generic.List`1[System.String]\n        // EffectIndex=0 Target=[entityName=驯化的雷象 id=78 zone=PLAY zonePos=1 cardId=SCH_714 player=1] SubOption=-1\n        internal val BLOCK_START_PATTERN =\n            Regex(\"BLOCK_START BlockType=(.*) Entity=(.*) EffectCardId=(.*) EffectIndex=(.*) Target=(.*) SubOption=(.*)\")\n\n        internal val BLOCK_START_CONTINUATION_PATTERN = Regex(\"(.*) TriggerKeyword=(.*)\")\n\n        //BLOCK_END\n        internal val BLOCK_END_PATTERN = Regex(\"BLOCK_END\")\n\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/parser/PowerTagHandler.kt",
    "content": "package com.ke.hs_tracker.module.parser\n\nimport com.ke.hs_tracker.module.db.Game\nimport com.ke.hs_tracker.module.domain.GetAllCardUseCase\nimport com.ke.hs_tracker.module.entity.*\nimport com.ke.hs_tracker.module.log\nimport com.ke.mvvm.base.data.successOr\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n/**\n * power tag处理器\n */\ninterface PowerTagHandler {\n\n\n    /**\n     * 处理一个事件\n     */\n    fun handle(powerTag: PowerTag)\n\n\n    /**\n     * 游戏事件流\n     */\n    val gameEventFlow: Flow<GameEvent?>\n}\n\nclass PowerTagHandlerImpl @Inject constructor(\n    private val getAllCardUseCase: GetAllCardUseCase\n) : PowerTagHandler {\n\n    lateinit var allCard: List<Card>\n\n\n    init {\n        GlobalScope.launch {\n            allCard = getAllCardUseCase(Unit).successOr(emptyList())\n        }\n    }\n\n\n    private val _gameEventFlow =\n//        Channel<GameEvent>(capacity = Channel.UNLIMITED)\n        MutableStateFlow<GameEvent?>(null)\n\n    override val gameEventFlow: Flow<GameEvent?>\n        get() = _gameEventFlow\n\n\n    /**\n     * 当前卡组\n     */\n//    var currentDeck: CurrentDeck? = null\n\n    /**\n     * 玩家\n     */\n    private var user: PowerTag.GameState.PlayerMapping? = null\n\n    /**\n     * 对手\n     */\n    private var opponent: PowerTag.GameState.PlayerMapping? = null\n\n    /**\n     * 当前回合数\n     */\n    private var currentTurn = 0\n\n\n    /**\n     * 游戏实体\n     */\n    private var game: Game = Game()\n\n    private val entityIdAndCardIdMap = mutableMapOf<Int, String>()\n\n\n    override fun handle(powerTag: PowerTag) {\n\n        when (powerTag) {\n            is PowerTag.GameState.BuildNumber -> {\n\n                game = Game(buildNumber = powerTag.number)\n//                game.userDeckName = currentDeck?.name ?: \"\"\n//                game.userDeckCode = currentDeck?.code ?: \"\"\n\n            }\n            is PowerTag.GameState.FormatType -> {\n                game.formatType = powerTag.type.toFormatType\n            }\n            is PowerTag.GameState.GameType -> {\n                game.gameType = powerTag.type.toGameType\n            }\n            is PowerTag.GameState.PlayerMapping -> {\n\n                if (powerTag.isUser) {\n                    game.userName = powerTag.name\n                    user = powerTag\n\n//                    game.isUserFirst = tag.first\n                } else {\n                    game.opponentName = powerTag.name\n                    opponent = powerTag\n\n                }\n\n\n            }\n            is PowerTag.GameState.ScenarioID -> {\n                game.scenarioID = powerTag.id.toInt()\n            }\n            is PowerTag.PowerTaskList.Block -> {\n                powerTag.list.forEach {\n                    handle(it)\n                }\n\n\n            }\n            is PowerTag.PowerTaskList.CreateGame -> {\n                //初始化卡牌\n                onGameStarted()\n//                gameEventListener(GameEvent.OnGameStarted)\n            }\n            is PowerTag.PowerTaskList.FullEntity -> {\n//                handleFullEntity(tag)\n                handleFullEntity(powerTag)\n                //FULL_ENTITY - Updating [entityName=萨尔 id=74 zone=PLAY zonePos=0 cardId=HERO_02 player=2] CardID=HERO_02\n                //    tag=CONTROLLER value=2\n                //    tag=CARDTYPE value=HERO\n                //    tag=HEALTH value=30\n                //    tag=ZONE value=PLAY\n                //    tag=ENTITY_ID value=74\n                //    tag=FACTION value=NEUTRAL\n                //    tag=RARITY value=FREE\n                //    tag=HERO_POWER value=687\n                //    tag=SPAWN_TIME_COUNT value=1\n                powerTag.cardId?.apply {\n                    entityIdAndCardIdMap[powerTag.entity.id] = this\n                }\n            }\n\n            is PowerTag.PowerTaskList.ShowEntity -> {\n                handleShowEntity(powerTag)\n                entityIdAndCardIdMap[powerTag.entity.id] = powerTag.cardId\n            }\n            is PowerTag.PowerTaskList.TagChange -> {\n                handleTagChange(powerTag)\n\n            }\n        }\n\n\n//        tag.toString().log()\n\n        (powerTag as? ZoneUpdatable)?.apply {\n\n\n            isUpdateZone(user?.id)?.let {\n                val cardId = it.cardId ?: entityIdAndCardIdMap[it.entityId]\n\n\n//                val event = ZonePositionChangedEvent(\n//                    entityId = it.first.id,\n//                    cardId = it.first.cardId,\n//                    isUser = it.first.player == user?.id,\n//                    currentZone = it.first.zone,\n//                    newZone = it.second,\n//                    currentPosition = it.first.zonePosition,\n//                    newPosition = it.first.zonePosition\n//                )\n//                zoneChangedEventList.add(it)\n                if (it.currentZone == Zone.Deck && it.newZone != Zone.Deck && it.isUser) {\n                    //从牌库中抽取一张卡\n                    //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=48 zone=DECK zonePos=0 cardId= player=2]\n                    //    tag=CONTROLLER value=2\n                    //    tag=CARDTYPE value=SPELL\n                    //    tag=TAG_LAST_KNOWN_COST_IN_HAND value=1\n                    //    tag=COST value=1\n                    //    tag=PREMIUM value=1\n                    //    tag=ZONE value=HAND\n                    //    tag=ENTITY_ID value=48\n                    //    tag=ELITE value=1\n                    //    tag=CLASS value=SHAMAN\n                    //    tag=RARITY value=LEGENDARY\n                    //    tag=478 value=2\n                    //    tag=QUEST_PROGRESS_TOTAL value=3\n                    //    tag=676 value=1\n                    //    tag=839 value=1\n                    //    tag=1043 value=1\n                    //    tag=1068 value=0\n                    //    tag=QUEST_REWARD_DATABASE_ID value=64323\n                    //    tag=SPAWN_TIME_COUNT value=1\n\n                    //或者 探底\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=3\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=0\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1037 value=1\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE value=HAND\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=6\n\n                    //探底抽上来的卡是没有cardId的\n                    if (cardId != null)\n                        removeCardFromDeck(cardId)\n\n                } else if (it.newZone == Zone.Deck && it.currentZone != Zone.Deck && it.isUser) {\n\n                    //当心探底\n                    //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] CardID=DED_002\n                    //    tag=CONTROLLER value=1\n                    //    tag=CARDTYPE value=SPELL\n                    //    tag=TAG_LAST_KNOWN_COST_IN_HAND value=2\n                    //    tag=COST value=2\n                    //    tag=ZONE value=DECK\n                    //    tag=ENTITY_ID value=28\n                    //    tag=RARITY value=RARE\n                    //    tag=DISCOVER value=1\n                    //    tag=478 value=1\n                    //    tag=1043 value=1\n                    //    tag=1068 value=0\n                    //    tag=USE_DISCOVER_VISUALS value=1\n                    //    tag=SPAWN_TIME_COUNT value=1\n                    //    tag=SPELL_SCHOOL value=1\n                    //    tag=1711 value=1\n                    //    tag=MINI_SET value=1\n\n\n                    //有牌插入到牌库\n                    //TAG_CHANGE Entity=[entityName=冷风 id=70 zone=HAND zonePos=2 cardId=AV_266 player=2] tag=ZONE value=DECK\n\n                    //会出现id为空的情况\n                    //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=17 zone=DECK zonePos=0 cardId= player=1] CardID=\n                    //    tag=ZONE value=DECK\n                    //    tag=CONTROLLER value=1\n                    //    tag=ENTITY_ID value=17\n                    if (cardId != null)\n                        insertCardToDeck(cardId)\n\n                } else if (it.currentZone == Zone.Play && it.newZone == Zone.Graveyard) {\n                    onGraveyardCardsChanged(cardId, it.isUser)\n                } else if (it.newZone == Zone.Hand || it.currentZone == Zone.Hand) {\n                    //手牌\n                    if (!it.isUser) {\n//                        handleOpponentHandChanged(it)\n\n                    }\n                }\n            }\n        }\n\n\n    }\n\n    private fun onGraveyardCardsChanged(cardId: String?, user: Boolean) {\n        if (cardId == null) {\n            return\n        }\n        _gameEventFlow.value = GameEvent.InsertCardToGraveyard(cardId, user)\n    }\n\n\n//    private fun findCardById(cardId: String) =\n//        allCard.find { it.id == cardId } ?: throw RuntimeException(\"id为 $cardId 没有这张牌\")\n\n\n    private fun removeCardFromDeck(cardId: String) {\n        _gameEventFlow.value = (GameEvent.RemoveCardFromUserDeck(cardId))\n    }\n\n    private fun insertCardToDeck(cardId: String) {\n        _gameEventFlow.value = (GameEvent.InsertCardToUserDeck(cardId))\n    }\n\n\n    private fun handleTagChange(tagChange: PowerTag.PowerTaskList.TagChange) {\n\n        tagChange.isTurnChanged()?.apply {\n            currentTurn = this\n        }\n\n        if (tagChange.entity.player == user?.id && tagChange.entity.zone == Zone.Hand && tagChange.tag == \"ZONE\" && tagChange.value == \"DECK\") {\n            //TAG_CHANGE Entity=[entityName=冷风 id=15 zone=HAND zonePos=3 cardId=AV_266 player=1] tag=ZONE value=DECK\n//            insertCardToDeck(tagChange.entity.cardId!!)\n        } else if (tagChange.tag == \"ZONE\" && tagChange.value == \"GRAVEYARD\" && tagChange.entity.zone == Zone.Play) {\n            //TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD\n            //随从死亡后进入墓地\n            //TAG_CHANGE Entity=[entityName=始生研习 id=63 zone=PLAY zonePos=0 cardId=SCH_270 player=2] tag=ZONE value=GRAVEYARD\n            //打出法术\n//            onGraveyardCardsChanged(tagChange.entity)\n\n        } else if (tagChange.isGameComplete) {\n            \"游戏结束了\".log()\n            onGameOver()\n        }\n\n        val pair = tagChange.isPlayerWonOrLost\n        if (pair != null) {\n            \"有玩家胜利或失败了 $pair $game\".log()\n            if (pair.first == game.userName) {\n                game.isUserWin = pair.second\n            } else {\n                game.opponentName = pair.first\n            }\n        }\n\n        if (tagChange.tag == \"FIRST_PLAYER\" && tagChange.value == \"1\") {\n            game.isUserFirst = tagChange.entity.entityName == game.userName\n        }\n\n    }\n\n    private fun onGameOver() {\n//        _deckLeftCardList.value = deckCardList\n        game.endTime = System.currentTimeMillis()\n        _gameEventFlow.value = GameEvent.OnGameOver(game)\n    }\n\n\n    private fun handleShowEntity(showEntity: PowerTag.PowerTaskList.ShowEntity) {\n        if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads[\"ZONE\"].equals(\n                \"hand\",\n                true\n            )\n        ) {\n\n            //起始手牌\n            //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=15 zone=DECK zonePos=0 cardId= player=1] CardID=AV_266\n            //    tag=CONTROLLER value=1\n            //    tag=CARDTYPE value=SPELL\n            //    tag=TAG_LAST_KNOWN_COST_IN_HAND value=1\n            //    tag=COST value=1\n            //    tag=PREMIUM value=1\n            //    tag=ZONE value=HAND\n            //    tag=ENTITY_ID value=15\n            //    tag=RARITY value=COMMON\n            //    tag=478 value=1\n            //    tag=1043 value=1\n            //    tag=1068 value=0\n            //    tag=SPAWN_TIME_COUNT value=1\n            //    tag=SPELL_SCHOOL value=3\n//            removeCardFromDeck(showEntity.cardId)\n        } else if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads[\"ZONE\"].equals(\n                \"GRAVEYARD\",\n                true\n            )\n        ) {\n            //爆牌\n            // ShowEntity(entity=Entity(entityName=UNKNOWN ENTITY, gameCardType=Invalid, id=54, zone=Deck, zonePosition=0, cardId=null, player=2), cardId=OG_176, payloads={CONTROLLER=2, CARDTYPE=SPELL, TAG_LAST_KNOWN_COST_IN_HAND=3, COST=3, ZONE=GRAVEYARD, ENTITY_ID=54, RARITY=COMMON, 478=2, 1037=2, 1043=1, 1068=0, SPAWN_TIME_COUNT=1, SPELL_SCHOOL=6})\n//            removeCardFromDeck(showEntity.cardId)\n\n        }\n    }\n\n    /**\n     * 游戏开始\n     */\n    private fun onGameStarted() {\n        \"游戏开始了\".log()\n        game.startTime = System.currentTimeMillis()\n        currentTurn = 0\n        _gameEventFlow.value = (GameEvent.OnGameStart)\n\n    }\n\n\n    private fun handleFullEntity(fullEntity: PowerTag.PowerTaskList.FullEntity) {\n        fullEntity.isUpdateHero()?.apply {\n\n            val cardClass = allCard.find {\n                it.id == second\n            }?.cardClass ?: return\n\n            \"更新英雄 $this $cardClass\".log()\n\n            if (first == user?.id) {\n                game.userHero = cardClass\n            } else if (first == opponent?.id) {\n                game.opponentHero = cardClass\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/service/ItemViewTouchListener.kt",
    "content": "package com.ke.hs_tracker.module.service\n\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.WindowManager\n\n\nclass ItemViewTouchListener(\n    private val wl: WindowManager.LayoutParams,\n    private val windowManager: WindowManager,\n    private val rootView: View\n) :\n    View.OnTouchListener {\n    private var x = 0\n    private var y = 0\n    override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {\n        when (motionEvent.action) {\n            MotionEvent.ACTION_DOWN -> {\n                x = motionEvent.rawX.toInt()\n                y = motionEvent.rawY.toInt()\n\n            }\n            MotionEvent.ACTION_MOVE -> {\n                val nowX = motionEvent.rawX.toInt()\n                val nowY = motionEvent.rawY.toInt()\n                val movedX = nowX - x\n                val movedY = nowY - y\n                x = nowX\n                y = nowY\n                wl.apply {\n                    x += movedX\n                    y += movedY\n                }\n                //更新悬浮球控件位置\n                windowManager.updateViewLayout(rootView, wl)\n            }\n            else -> {\n\n            }\n        }\n        return false\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/service/ScaleTouchListener.kt",
    "content": "package com.ke.hs_tracker.module.service\n\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.WindowManager\n\nclass ScaleTouchListener constructor(\n    private val windowManager: WindowManager,\n    private val rootView: View,\n    private val layoutParams: WindowManager.LayoutParams\n) : View.OnTouchListener {\n    private var x = 0\n    private var y = 0\n\n    override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {\n        when (motionEvent.action) {\n            MotionEvent.ACTION_DOWN -> {\n                x = motionEvent.rawX.toInt()\n                y = motionEvent.rawY.toInt()\n\n            }\n            MotionEvent.ACTION_MOVE -> {\n                val nowX = motionEvent.rawX.toInt()\n                val nowY = motionEvent.rawY.toInt()\n                val movedX = nowX - x\n                val movedY = nowY - y\n                x = nowX\n                y = nowY\n                layoutParams.apply {\n//                        x += movedX\n//                        y += movedY\n                    width += movedX\n                    height += movedY\n\n                }\n                //更新悬浮球控件位置\n//                windowManager.updateViewLayout(rootView, layoutParams)\n                windowManager.updateViewLayout(rootView, layoutParams)\n            }\n            else -> {\n\n            }\n        }\n        return true\n    }\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/service/WindowService.kt",
    "content": "package com.ke.hs_tracker.module.service\n\nimport android.content.Intent\nimport android.os.Build\nimport android.os.IBinder\nimport android.view.Gravity\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.WindowManager\nimport android.widget.AdapterView\nimport android.widget.AdapterView.OnItemSelectedListener\nimport android.widget.ArrayAdapter\nimport androidx.lifecycle.LifecycleService\nimport androidx.lifecycle.lifecycleScope\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleFloatingWindowBinding\nimport com.ke.hs_tracker.module.log\nimport com.ke.hs_tracker.module.parser.DeckCardObserver\nimport com.ke.hs_tracker.module.ui.common.CardAdapter\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass WindowService : LifecycleService() {\n\n\n    private val windowManager: WindowManager by lazy {\n        getSystemService(WINDOW_SERVICE) as WindowManager\n    }\n\n    private val binding: ModuleFloatingWindowBinding by lazy {\n        val layoutInflater = LayoutInflater.from(applicationContext)\n        ModuleFloatingWindowBinding.inflate(layoutInflater)\n    }\n\n    private val deckAdapter = CardAdapter()\n\n    private val graveyardAdapter = CardAdapter()\n\n    private val opponentGraveyardAdapter = CardAdapter()\n\n    @Inject\n    lateinit var deckCardObserver: DeckCardObserver\n\n    private var showList = false\n\n\n    private fun showView() {\n        val layoutParams = WindowManager.LayoutParams()\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;\n        } else {\n            layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;\n        }\n\n        layoutParams.width = resources.getDimension(R.dimen.module_floating_window_width).toInt()\n        layoutParams.height =\n//            LayoutParams.WRAP_CONTENT\n            resources.getDimension(R.dimen.module_floating_window_height).toInt()\n        //需要设置 这个 不然空白地方无法点击\n        layoutParams.flags =\n            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE\n//        layoutParams.alpha = 0.8f\n\n        layoutParams.gravity = Gravity.START or Gravity.TOP\n\n        windowManager.addView(binding.root, layoutParams)\n\n        binding.recyclerView.adapter = deckAdapter\n\n        binding.scale.setOnTouchListener(\n            ScaleTouchListener(windowManager, binding.root, layoutParams)\n        )\n\n        binding.hide.setOnClickListener {\n//            binding.recyclerView.isVisible = !binding.recyclerView.isVisible\n            layoutParams.height = if (showList) {\n                resources.getDimension(R.dimen.module_floating_window_height).toInt()\n            } else {\n                resources.getDimension(R.dimen.module_floating_window_header_height).toInt()\n            }\n            windowManager.updateViewLayout(binding.root, layoutParams)\n\n            showList = !showList\n        }\n\n        binding.close.setOnClickListener {\n            windowManager.removeView(binding.root)\n            stopSelf()\n        }\n\n        binding.spinner.adapter = ArrayAdapter.createFromResource(\n            applicationContext,\n            R.array.module_spinner,\n            android.R.layout.simple_list_item_1\n        )\n        binding.spinner.onItemSelectedListener = object : OnItemSelectedListener {\n            override fun onItemSelected(\n                parent: AdapterView<*>,\n                view: View,\n                position: Int,\n                id: Long\n            ) {\n                val adapter = when (position) {\n                    0 -> {\n                        deckAdapter\n                    }\n                    1 -> {\n                        graveyardAdapter\n                    }\n                    2 -> {\n                        opponentGraveyardAdapter\n                    }\n                    else -> throw  IllegalArgumentException(\"错误的position $position\")\n                }\n\n                binding.recyclerView.adapter = adapter\n                windowManager.updateViewLayout(binding.root, layoutParams)\n            }\n\n            override fun onNothingSelected(parent: AdapterView<*>?) {\n\n            }\n\n        }\n\n        binding.spinner.setSelection(0)\n\n        binding.root.setOnTouchListener(\n            ItemViewTouchListener(\n                layoutParams,\n                windowManager,\n                binding.root\n            )\n        )\n    }\n\n    override fun onCreate() {\n        super.onCreate()\n\n        showView()\n\n        deckCardObserver.init(lifecycleScope)\n\n//        lifecycleScope.launch {\n//            delay(5000)\n//            throw RuntimeException(\"测试异常\")\n//        }\n\n\n        lifecycleScope.launch {\n            deckCardObserver.deckCardList.collect {\n//                adapter.setList(it)\n                deckAdapter.setDiffNewData(it.toMutableList())\n            }\n        }\n\n        lifecycleScope.launch {\n            deckCardObserver.userGraveyardCardList.collect {\n                graveyardAdapter.setDiffNewData(it.toMutableList())\n            }\n        }\n\n        lifecycleScope.launch {\n            deckCardObserver.opponentGraveyardCardList.collect {\n                opponentGraveyardAdapter.setDiffNewData(it.toMutableList())\n            }\n        }\n\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        \"service 挂了\".log()\n    }\n\n    override fun onBind(intent: Intent): IBinder? {\n        return null\n    }\n\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/chart/GetSummaryChartViewDataUseCase.kt",
    "content": "package com.ke.hs_tracker.module.ui.chart\n\nimport com.ke.hs_tracker.module.db.GameDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\ninternal class GetSummaryChartViewDataUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    private val gameDao: GameDao\n) : UseCase<Unit, SummaryChartViewData>(dispatcher) {\n\n    override suspend fun execute(parameters: Unit): SummaryChartViewData {\n        val games = gameDao.getAll()\n        val winCount = games.count {\n            it.isUserWin == true\n        }\n        val lossCount = games.count {\n            it.isUserWin == false\n        }\n        val firstHandCount = games.count {\n            it.isUserFirst == true\n        }\n        val secondHandCount = games.count {\n            it.isUserFirst == false\n        }\n\n        val firstHandWinCount = games.count {\n            it.isUserFirst == true && it.isUserWin == true\n        }\n\n        val firstHandLossCount = games.count {\n            it.isUserFirst == true && it.isUserWin == false\n        }\n        val secondHandWinCount = games.count {\n            it.isUserFirst == false && it.isUserWin == true\n        }\n\n        val secondHandLossCount = games.count {\n            it.isUserFirst == false && it.isUserWin == false\n        }\n\n        val classCounts = CardClass.values()\n            .filter {\n                it.isHero\n            }.map {\n                it to games.count { game ->\n                    game.opponentHero == it\n                }\n            }\n\n        return SummaryChartViewData(\n            winCount,\n            lossCount,\n            firstHandCount,\n            secondHandCount,\n            firstHandWinCount,\n            firstHandLossCount,\n            secondHandWinCount,\n            secondHandLossCount,\n            classCounts\n        )\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/chart/PieChartData.kt",
    "content": "package com.ke.hs_tracker.module.ui.chart\n\nimport androidx.annotation.ColorRes\n\n data class PieChartData(\n     val label: String,\n     val value: Int,\n     @ColorRes\n     val color: Int\n )\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/chart/SummaryChartActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.chart\n\nimport android.graphics.Color\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport com.github.mikephil.charting.charts.PieChart\nimport com.github.mikephil.charting.data.PieData\nimport com.github.mikephil.charting.data.PieDataSet\nimport com.github.mikephil.charting.data.PieEntry\nimport com.github.mikephil.charting.formatter.PercentFormatter\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivitySummaryChartBinding\nimport com.ke.mvvm.base.data.Result\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass SummaryChartActivity : AppCompatActivity() {\n\n\n    @Inject\n    internal lateinit var getSummaryChartViewDataUseCase: GetSummaryChartViewDataUseCase\n\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n//        setContentView(R.layout.module_activity_summary_chart)\n        val binding: ModuleActivitySummaryChartBinding =\n            ModuleActivitySummaryChartBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n\n//        binding.all.apply {\n//            //设置成实心\n//            holeRadius = 0f\n//            transparentCircleRadius = 0f\n//            //禁用描述\n//            description.isEnabled = false\n//            //禁用颜色描述\n//            legend.isEnabled = false\n//            setUsePercentValues(true)\n//            val pieEntryList = mutableListOf<PieEntry>()\n//            pieEntryList.add(PieEntry(41f, \"胜利41\"))\n//            pieEntryList.add(PieEntry(60f, \"失败60\"))\n//            val pieDataSet = PieDataSet(pieEntryList, \"\")\n//            pieDataSet.valueTextSize = 20f\n//            pieDataSet.valueTextColor = Color.WHITE\n//\n//            pieDataSet.colors = listOf(R.color.module_win, R.color.module_loss).map {\n//                resources.getColor(it, null)\n//            }\n//            val pieData = PieData(pieDataSet)\n//            //显示次数\n//            pieData.setDrawValues(true)\n//            pieData.setValueFormatter(PercentFormatter())\n//            data = pieData\n//            invalidate()\n//        }\n\n\n        lifecycleScope.launch {\n            val result = getSummaryChartViewDataUseCase(Unit)\n            when (result) {\n                is Result.Success -> {\n                    setChartData(result.data, binding)\n                }\n                is Result.Error -> {\n                    result.exception.printStackTrace()\n                }\n            }\n        }\n\n\n    }\n\n    private fun setChartData(\n        summaryChartViewData: SummaryChartViewData,\n        binding: ModuleActivitySummaryChartBinding\n    ) {\n\n        setupPieChart(\n            listOf(\n                PieChartData(\n                    \"${getString(R.string.module_win)}${summaryChartViewData.winCount}\",\n                    summaryChartViewData.winCount,\n                    R.color.module_win\n                ),\n                PieChartData(\n                    \"${getString(R.string.module_loss)}${summaryChartViewData.lossCount}\",\n                    summaryChartViewData.lossCount,\n                    R.color.module_loss\n                ),\n            ),\n            binding.all\n        )\n\n        setupPieChart(\n            listOf(\n                PieChartData(\n                    \"${getString(R.string.module_first_hand)}${summaryChartViewData.firstHandCount}\",\n                    summaryChartViewData.firstHandCount,\n                    R.color.module_win\n                ),\n                PieChartData(\n                    \"${getString(R.string.module_second_hand)}${summaryChartViewData.secondHandCount}\",\n                    summaryChartViewData.secondHandCount,\n                    R.color.module_loss\n                ),\n            ),\n            binding.firstHandPercent\n        )\n\n        setupPieChart(\n            listOf(\n                PieChartData(\n                    \"${getString(R.string.module_win)}${summaryChartViewData.firstHandWinCount}\",\n                    summaryChartViewData.firstHandWinCount,\n                    R.color.module_win\n                ),\n                PieChartData(\n                    \"${getString(R.string.module_loss)}${summaryChartViewData.firstHandLossCount}\",\n                    summaryChartViewData.firstHandLossCount,\n                    R.color.module_loss\n                ),\n            ),\n            binding.firstHandRate\n        )\n\n        setupPieChart(\n            listOf(\n                PieChartData(\n                    \"${getString(R.string.module_win)}${summaryChartViewData.secondHandWinCount}\",\n                    summaryChartViewData.secondHandWinCount,\n                    R.color.module_win\n                ),\n                PieChartData(\n                    \"${getString(R.string.module_loss)}${summaryChartViewData.secondHandLossCount}\",\n                    summaryChartViewData.secondHandLossCount,\n                    R.color.module_loss\n                ),\n            ),\n            binding.secondHandRate\n        )\n\n        setupPieChart(\n            summaryChartViewData.classCounts.map {\n                PieChartData(\n                    getString(it.first.titleRes) + it.second,\n                    it.second,\n                    it.first.color\n                )\n            },\n            binding.classDistribution\n        )\n    }\n\n    private fun setupPieChart(list: List<PieChartData>, pieChart: PieChart) {\n        pieChart.apply {\n            //设置成实心\n            holeRadius = 0f\n            transparentCircleRadius = 0f\n            //禁用描述\n            description.isEnabled = false\n            //禁用颜色描述\n            legend.isEnabled = false\n            setUsePercentValues(true)\n\n            val pieDataSet = PieDataSet(list.map {\n                return@map PieEntry(\n                    it.value.toFloat(), it.label\n                )\n            }, \"\")\n            pieDataSet.valueTextSize = 16f\n            pieDataSet.valueTextColor = Color.WHITE\n\n            pieDataSet.colors = list.map {\n                it.color\n            }.map {\n                resources.getColor(it, null)\n            }\n\n            val pieData = PieData(pieDataSet)\n//            pieData.setDrawValues(true)\n            pieData.setValueFormatter(PercentFormatter(this))\n\n//            pieData.setValueFormatter(object : ValueFormatter() {\n//                override fun getFormattedValue(value: Float): String {\n//                    return \"$value %\"\n//                }\n//            })\n\n            data = pieData\n            invalidate()\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/chart/SummaryChartViewData.kt",
    "content": "package com.ke.hs_tracker.module.ui.chart\n\nimport com.ke.hs_tracker.module.entity.CardClass\n\ninternal data class SummaryChartViewData(\n    /**\n     * 总胜利次数\n     */\n    val winCount: Int,\n    /**\n     * 总失败次数\n     */\n    val lossCount: Int,\n    /**\n     * 先手次数\n     */\n    val firstHandCount: Int,\n    /**\n     * 后手次数\n     */\n    val secondHandCount: Int,\n    /**\n     * 先手胜利次数\n     */\n    val firstHandWinCount: Int,\n    /**\n     * 先手失败次数\n     */\n    val firstHandLossCount: Int,\n    /**\n     * 后手胜利次数\n     */\n    val secondHandWinCount: Int,\n    /**\n     * 后手失败次数\n     */\n    val secondHandLossCount: Int,\n    /**\n     * 职业出现频度\n     */\n    val classCounts: List<Pair<CardClass, Int>>\n)\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleDetailActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.classbattledetail\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport com.ke.hs_tracker.module.databinding.ModuleActivityClassBattleDetailBinding\nimport com.ke.hs_tracker.module.databinding.ModuleItemClassBattleDetailBinding\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.mvvm.base.data.ViewStatus\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass ClassBattleDetailActivity : AppCompatActivity() {\n\n    internal val viewModel: ClassBattleDetailViewModel by viewModels()\n\n    private val adapter by lazy {\n        object : BaseViewBindingAdapter<ClassBattleItem, ModuleItemClassBattleDetailBinding>() {\n            override fun bindItem(\n                item: ClassBattleItem,\n                viewBinding: ModuleItemClassBattleDetailBinding,\n                viewType: Int,\n                position: Int\n            ) {\n\n                viewBinding.apply {\n                    value1.setText(item.hero.titleRes)\n                    value2.text = item.times.toString()\n                    value3.text = item.win.toString()\n                    value4.text = item.loss\n                        .toString()\n                    value5.text = \"${item.rate}%\"\n                }\n            }\n\n            override fun createViewBinding(\n                inflater: LayoutInflater,\n                parent: ViewGroup,\n                viewType: Int\n            ): ModuleItemClassBattleDetailBinding {\n                return ModuleItemClassBattleDetailBinding.inflate(inflater, parent, false)\n            }\n\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n//        setContentView(R.layout.module_activity_class_battle_detail)\n        val binding = ModuleActivityClassBattleDetailBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n\n        binding.recyclerView.adapter = adapter\n        binding.recyclerView.addItemDecoration(\n            DividerItemDecoration(\n                this,\n                DividerItemDecoration.VERTICAL\n            )\n        )\n\n        launchAndRepeatWithViewLifecycle {\n            viewModel.viewStatus.collect {\n                when (it) {\n                    is ViewStatus.Loading -> {\n\n                    }\n                    is ViewStatus.Content -> {\n                        adapter.setList(it.data)\n                    }\n                    is ViewStatus.Error -> {\n\n                    }\n                }\n            }\n        }\n    }\n\n    companion object {\n        fun createIntent(context: Context, cardClass: CardClass): Intent {\n            return Intent(context, ClassBattleDetailActivity::class.java).apply {\n                putExtra(EXTRA_CARD_CLASS, cardClass)\n            }\n        }\n\n        internal const val EXTRA_CARD_CLASS = \"extra_card_class\"\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleDetailViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.classbattledetail\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.mvvm.base.data.successOr\nimport com.ke.mvvm.base.ui.BaseContentViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\ninternal class ClassBattleDetailViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val getClassBattleItemListUseCase: GetClassBattleItemListUseCase\n) :\n    BaseContentViewModel<List<ClassBattleItem>>() {\n\n    internal val cardClass: CardClass =\n        savedStateHandle.get<CardClass>(ClassBattleDetailActivity.EXTRA_CARD_CLASS)!!\n\n    init {\n        viewModelScope.launch {\n            showLoading()\n            showContent(getClassBattleItemListUseCase(cardClass).successOr(emptyList()))\n        }\n    }\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleItem.kt",
    "content": "package com.ke.hs_tracker.module.ui.classbattledetail\n\nimport com.ke.hs_tracker.module.entity.CardClass\n\ninternal data class ClassBattleItem(\n    val hero: CardClass,\n    val times: Int,\n    val win: Int,\n    val loss: Int,\n    val rate: Int\n)\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/GetClassBattleItemListUseCase.kt",
    "content": "package com.ke.hs_tracker.module.ui.classbattledetail\n\nimport com.ke.hs_tracker.module.db.GameDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\ninternal class GetClassBattleItemListUseCase @Inject constructor(\n    @IoDispatcher\n    dispatcher: CoroutineDispatcher,\n    private val dao: GameDao\n) : UseCase<CardClass, List<ClassBattleItem>>(dispatcher) {\n\n    override suspend fun execute(parameters: CardClass): List<ClassBattleItem> {\n\n        val items = mutableListOf<ClassBattleItem>()\n        dao.getByHero(parameters)\n            .groupBy {\n                it.opponentHero!!\n            }.forEach { cardClass, list ->\n                val times = list.size\n                val win = list.count {\n                    it.isUserWin == true\n                }\n                val loss = times - win\n                val rate = win * 100 / times\n                val item = ClassBattleItem(cardClass, times, win, loss, rate)\n\n                items.add(item)\n            }\n\n        return items\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/common/CardAdapter.kt",
    "content": "package com.ke.hs_tracker.module.ui.common\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.DiffUtil\nimport com.ke.hs_tracker.module.bindCard\nimport com.ke.hs_tracker.module.databinding.ModuleItemCardBinding\nimport com.ke.hs_tracker.module.entity.CardBean\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\n\nclass CardAdapter : BaseViewBindingAdapter<CardBean, ModuleItemCardBinding>() {\n\n\n\n    init {\n\n        setDiffCallback(object : DiffUtil.ItemCallback<CardBean>() {\n            override fun areItemsTheSame(oldItem: CardBean, newItem: CardBean): Boolean {\n                return oldItem == newItem\n            }\n\n            override fun areContentsTheSame(oldItem: CardBean, newItem: CardBean): Boolean {\n                return oldItem == newItem\n            }\n\n        })\n    }\n\n    override fun bindItem(\n        item: CardBean,\n        viewBinding: ModuleItemCardBinding,\n        viewType: Int,\n        position: Int\n    ) {\n\n        viewBinding.bindCard(item.card)\n//        viewBinding.name.text = item.card.name\n//        viewBinding.cost.text = item.card.cost.toString()\n        viewBinding.count.text = item.count.toString()\n\n//        item.card.rarity?.apply {\n//            viewBinding.name.setTextColor(\n//                ResourcesCompat.getColor(\n//                    viewBinding.root.context.resources,\n//                    colorRes,\n//                    null\n//                )\n//            )\n//\n//        }\n//\n//        Glide.with(viewBinding.imageTile)\n//            .load(\"https://art.hearthstonejson.com/v1/tiles/${item.card.id}.png\")\n//            .into(viewBinding.imageTile)\n    }\n\n    override fun createViewBinding(\n        inflater: LayoutInflater,\n        parent: ViewGroup,\n        viewType: Int\n    ): ModuleItemCardBinding {\n        return ModuleItemCardBinding.inflate(inflater, parent, false)\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/common/LoadingFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.common\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.databinding.ModuleFragmentLoadingBinding\n\nclass LoadingFragment : Fragment() {\n    private val binding:ModuleFragmentLoadingBinding by viewbind()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return binding.root\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deck/DeckCodeParserActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.deck\n\nimport android.os.Bundle\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport com.ke.hs_tracker.module.databinding.ModuleActivityDeckCodeParserBinding\nimport com.ke.hs_tracker.module.ui.common.CardAdapter\nimport com.ke.mvvm.base.data.ViewStatus\nimport com.ke.mvvm.base.ui.collectLoadingDialog\nimport com.ke.mvvm.base.ui.collectSnackbarFlow\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass DeckCodeParserActivity : AppCompatActivity() {\n    private val viewModel: DeckCodeParserViewModel by viewModels()\n    private lateinit var binding: ModuleActivityDeckCodeParserBinding\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityDeckCodeParserBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        collectLoadingDialog(viewModel)\n        collectSnackbarFlow(viewModel)\n        binding.recyclerView.addItemDecoration(\n            DividerItemDecoration(\n                this,\n                DividerItemDecoration.VERTICAL\n            )\n        )\n        val adapter = CardAdapter()\n        binding.recyclerView.adapter = adapter\n        binding.start.setOnClickListener {\n            viewModel.start(\n                binding.code.text?.toString() ?: \"\"\n            )\n        }\n\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n        launchAndRepeatWithViewLifecycle {\n            viewModel.viewStatus.collect {\n                when (it) {\n                    is ViewStatus.Loading -> {\n\n                    }\n                    is ViewStatus.Content -> {\n\n                        adapter.setList(it.data.apply {\n                        })\n\n                    }\n                    is ViewStatus.Error -> {\n\n                    }\n                }\n            }\n        }\n    }\n\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deck/DeckCodeParserViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.deck\n\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase\nimport com.ke.hs_tracker.module.entity.CardBean\nimport com.ke.mvvm.base.data.successOr\nimport com.ke.mvvm.base.ui.BaseContentViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass DeckCodeParserViewModel @Inject constructor(private val parseDeckCodeUseCase: ParseDeckCodeUseCase) :\n    BaseContentViewModel<List<CardBean>>() {\n\n\n    fun start(code: String) {\n        viewModelScope.launch {\n            showLoadingDialog(\"加载中\")\n            val list = parseDeckCodeUseCase(code).successOr(emptyList())\n            dismissLoadingDialog()\n            showContent(list)\n//            when (result) {\n//                is Result.Success -> {\n//                    showContent(result.data)\n//                }\n//                is Result.Error -> {\n//                    result.exception.printStackTrace()\n//                }\n//            }\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/BattleRecordsFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.activityViewModels\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.ui.records.RecordAdapter\nimport com.ke.mvvm.base.databinding.KeMvvmLayoutBaseRefreshListRetryBinding\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass BattleRecordsFragment : Fragment() {\n    private val battleRecordsViewModel: BattleRecordsViewModel by activityViewModels()\n\n    private val binding: KeMvvmLayoutBaseRefreshListRetryBinding by viewbind()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return binding.root\n    }\n\n    private val adapter by lazy {\n        RecordAdapter()\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        binding.swipeRefreshLayout.isEnabled = false\n        binding.recyclerView.adapter = adapter\n        binding.recyclerView.addItemDecoration(\n            DividerItemDecoration(\n                requireContext(),\n                DividerItemDecoration.VERTICAL\n            )\n        )\n        launchAndRepeatWithViewLifecycle {\n            battleRecordsViewModel.games.collect {\n                adapter.setList(it)\n            }\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/BattleRecordsViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.db.Game\nimport com.ke.mvvm.base.data.successOr\nimport com.ke.mvvm.base.ui.BaseViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass BattleRecordsViewModel @Inject constructor(\n    private val getGamesByDeckCodeAndNameUseCase: GetGamesByDeckCodeAndNameUseCase,\n    savedStateHandle: SavedStateHandle\n) : BaseViewModel() {\n\n    private val _games = MutableStateFlow<List<Game>>(emptyList())\n\n    internal val games: StateFlow<List<Game>>\n        get() = _games\n\n    init {\n\n        viewModelScope.launch {\n            val code = savedStateHandle.get<String>(DeckBattleDetailActivity.EXTRA_DECK_CODE)!!\n            val name = savedStateHandle.get<String>(DeckBattleDetailActivity.EXTRA_DECK_NAME)!!\n            _games.value = getGamesByDeckCodeAndNameUseCase(code to name).successOr(emptyList())\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckBattleDetailActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.fragment.app.Fragment\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport com.google.android.material.tabs.TabLayoutMediator\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivityDeckBattleDetailBinding\nimport dagger.hilt.android.AndroidEntryPoint\n\n@AndroidEntryPoint\nclass DeckBattleDetailActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val binding = ModuleActivityDeckBattleDetailBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n        val fragmentList = listOf(\n            SummaryFragment(),\n            BattleRecordsFragment(),\n            DeckFragment(),\n            DeckDetailFragment()\n        )\n        val titles = listOf(\n            getString(R.string.module_summary),\n            getString(R.string.module_record),\n            getString(R.string.module_deck),\n            getString(\n                R.string.module_deck_detail\n            )\n        )\n        val adapter = object : FragmentStateAdapter(this) {\n            override fun getItemCount(): Int {\n                return fragmentList.size\n            }\n\n            override fun createFragment(position: Int): Fragment {\n                return fragmentList[position]\n            }\n        }\n        binding.viewPager.adapter = adapter\n        TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, index ->\n            tab.text = titles[index]\n        }.attach()\n    }\n\n\n    companion object {\n        fun createIntent(context: Context, deckCode: String, deckName: String): Intent {\n            return Intent(context, DeckBattleDetailActivity::class.java).apply {\n                putExtra(EXTRA_DECK_NAME, deckName)\n                putExtra(EXTRA_DECK_CODE, deckCode)\n            }\n        }\n\n        internal const val EXTRA_DECK_CODE = \"EXTRA_DECK_CODE\"\n        internal const val EXTRA_DECK_NAME = \"EXTRA_DECK_NAME\"\n\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckDetailFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.activityViewModels\nimport com.github.mikephil.charting.charts.BarChart\nimport com.github.mikephil.charting.components.XAxis\nimport com.github.mikephil.charting.data.BarData\nimport com.github.mikephil.charting.data.BarDataSet\nimport com.github.mikephil.charting.data.BarEntry\nimport com.github.mikephil.charting.formatter.ValueFormatter\nimport com.google.android.material.chip.Chip\nimport com.google.android.material.chip.ChipGroup\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.databinding.ModuleFragmentDeckBattleDetailDeckDetailBinding\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass DeckDetailFragment : Fragment() {\n    private val binding: ModuleFragmentDeckBattleDetailDeckDetailBinding by viewbind()\n\n    private val deckDetailViewModel: DeckDetailViewModel by activityViewModels()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        launchAndRepeatWithViewLifecycle {\n            deckDetailViewModel.costList.collect {\n                fillChart(\n                    binding.costChart, it\n                )\n            }\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            deckDetailViewModel.attackList.collect {\n                fillChart(\n                    binding.attackChart, it\n                )\n            }\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            deckDetailViewModel.mechanics.collect { list ->\n                fillChips(\n                    binding.mechanicsChips,\n                    list.map {\n                        it.first.name + it.second\n                    }\n                )\n            }\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            deckDetailViewModel.raceList.collect { list ->\n                fillChips(binding.raceChips, list.map {\n                    getString(it.first.titleRes!!) + it.second\n                })\n            }\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            deckDetailViewModel.spellSchoolList.collect { list ->\n                fillChips(\n                    binding.spellSchoolChips,\n                    list.map {\n                        getString(it.first.titleRes) + it.second\n                    }\n                )\n            }\n        }\n    }\n\n    private fun fillChips(chipGroup: ChipGroup, strings: List<String>) {\n        chipGroup.removeAllViews()\n        strings.map {\n            Chip(requireContext()).apply {\n                text = it\n            }\n        }.forEach {\n            chipGroup.addView(it)\n        }\n\n    }\n\n    private fun fillChart(barChart: BarChart, list: List<Pair<Int, Int>>) {\n        barChart.description.isEnabled = false\n        barChart.xAxis.apply {\n            setDrawGridLines(false)\n            position = XAxis.XAxisPosition.BOTTOM\n            labelCount = list.size\n            textColor = Color.WHITE\n        }\n\n        barChart.axisLeft.apply {\n            setDrawAxisLine(false)\n        }\n\n        barChart.legend.isEnabled = false\n\n        val entryList = list.map {\n            BarEntry(it.first.toFloat(), it.second.toFloat())\n        }\n        val barDataSet = BarDataSet(entryList, \"\")\n\n//        barDataSet.setValueTextColors(listOf(Color.WHITE))\n        barDataSet.valueTextColor = Color.WHITE\n        barDataSet.valueTextSize = 14f\n        barDataSet.valueFormatter = object : ValueFormatter() {\n            override fun getFormattedValue(value: Float): String {\n                return value.toInt().toString()\n            }\n        }\n\n        barChart.data = BarData(barDataSet)\n        barChart.setFitBars(true)\n        barChart.invalidate()\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckDetailViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase\nimport com.ke.hs_tracker.module.entity.CardType\nimport com.ke.hs_tracker.module.entity.Mechanics\nimport com.ke.hs_tracker.module.entity.Race\nimport com.ke.hs_tracker.module.entity.SpellSchool\nimport com.ke.mvvm.base.data.successOr\nimport com.ke.mvvm.base.ui.BaseViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass DeckDetailViewModel\n@Inject constructor(\n    private val parseDeckCodeUseCase: ParseDeckCodeUseCase,\n    savedStateHandle: SavedStateHandle\n) : BaseViewModel() {\n\n    private val _costList = MutableStateFlow<List<Pair<Int, Int>>>(emptyList())\n\n    /**\n     * 法力曲线\n     */\n    internal val costList: StateFlow<List<Pair<Int, Int>>>\n        get() = _costList\n\n    private val _attackList = MutableStateFlow<List<Pair<Int, Int>>>(emptyList())\n\n    /**\n     * 攻击力曲线\n     */\n    internal val attackList: StateFlow<List<Pair<Int, Int>>>\n        get() = _attackList\n\n    private val _mechanics = MutableStateFlow<List<Pair<Mechanics, Int>>>(emptyList())\n\n    /**\n     * 卡牌效果\n     */\n    internal val mechanics: StateFlow<List<Pair<Mechanics, Int>>>\n        get() = _mechanics\n\n    private val _raceList = MutableStateFlow<List<Pair<Race, Int>>>(emptyList())\n\n    //随从种族\n    internal val raceList: StateFlow<List<Pair<Race, Int>>>\n        get() = _raceList\n\n    private val _spellSchoolList = MutableStateFlow<List<Pair<SpellSchool, Int>>>(emptyList())\n    internal val spellSchoolList: StateFlow<List<Pair<SpellSchool, Int>>>\n        get() = _spellSchoolList\n\n    init {\n        viewModelScope.launch {\n            val code = savedStateHandle.get<String>(DeckBattleDetailActivity.EXTRA_DECK_CODE)!!\n\n            val cardList = parseDeckCodeUseCase(code).successOr(emptyList())\n            _costList.value = cardList\n                .flatMap {\n                    it.toCardList()\n                }\n                .groupBy {\n                    val cost =\n                        it.cost\n                    if (cost > MAX_VALUE) MAX_VALUE else cost\n                }\n                .map {\n                    it.key to it.value.size\n                }\n\n            _attackList.value = cardList\n                .flatMap {\n                    it.toCardList()\n                }\n                .filter {\n                    //必须是随从牌或武器牌\n                    it.type == CardType.Minion || it.type == CardType.Weapon\n                }.groupBy {\n                    if (\n                        it.attack >= MAX_VALUE\n                    ) MAX_VALUE else it.attack\n                }.map {\n                    it.key to it.value.size\n                }\n\n\n\n            _mechanics.value = cardList\n                .flatMap {\n                    it.toCardList()\n                }\n                .flatMap {\n                    it.mechanics\n                }.groupBy {\n                    it\n                }.map {\n                    it.key to it.value.size\n                }\n\n            val raceMap = mutableMapOf<Race, Int>()\n\n            var allRace = 0\n\n            cardList.flatMap {\n                it.toCardList()\n            }.mapNotNull {\n                it.race\n            }.forEach {\n                if (it.tradition) {\n                    if (it != Race.All) {\n                        var count = raceMap[it] ?: 0\n                        count += 1\n                        raceMap[it] = count\n                    } else if (it == Race.All) {\n                        allRace += 1\n                    }\n                }\n            }\n\n            _raceList.value = raceMap.map {\n                it.key to it.value + allRace\n            }\n\n            _spellSchoolList.value = cardList.flatMap {\n                it.toCardList()\n            }.mapNotNull {\n                it.spellSchool\n            }.groupBy {\n                it\n            }.map {\n                it.key to it.value.size\n            }\n\n        }\n    }\n}\n\nprivate const val MAX_VALUE = 7"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport androidx.fragment.app.activityViewModels\nimport com.ke.hs_tracker.module.entity.CardBean\nimport com.ke.hs_tracker.module.ui.main.CardListFragment\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.StateFlow\n\n@AndroidEntryPoint\nclass DeckFragment : CardListFragment() {\n\n    private val deckViewModel: DeckViewModel by activityViewModels()\n\n    override val cardList: StateFlow<List<CardBean>>\n        get() = deckViewModel.cardList\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase\nimport com.ke.hs_tracker.module.entity.CardBean\nimport com.ke.mvvm.base.data.successOr\nimport com.ke.mvvm.base.ui.BaseViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass DeckViewModel @Inject constructor(\n    private val parseDeckCodeUseCase: ParseDeckCodeUseCase,\n    savedStateHandle: SavedStateHandle\n) : BaseViewModel() {\n\n    private val _cardList = MutableStateFlow(emptyList<CardBean>())\n    internal val cardList: StateFlow<List<CardBean>>\n        get() = _cardList\n\n    init {\n        viewModelScope.launch {\n            val code = savedStateHandle.get<String>(DeckBattleDetailActivity.EXTRA_DECK_CODE)!!\n\n            _cardList.value = parseDeckCodeUseCase(code).successOr(emptyList())\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/GetGamesByDeckCodeAndNameUseCase.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport com.ke.hs_tracker.module.db.Game\nimport com.ke.hs_tracker.module.db.GameDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass GetGamesByDeckCodeAndNameUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    private val gameDao: GameDao\n) : UseCase<Pair<String, String>, List<Game>>(dispatcher) {\n\n    override suspend fun execute(parameters: Pair<String, String>): List<Game> {\n\n        return gameDao.getByDeckCodeAndName(\n            parameters.first,\n            parameters.second\n        )\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/SummaryFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.activityViewModels\nimport com.github.mikephil.charting.charts.PieChart\nimport com.github.mikephil.charting.components.Legend\nimport com.github.mikephil.charting.data.PieData\nimport com.github.mikephil.charting.data.PieDataSet\nimport com.github.mikephil.charting.data.PieEntry\nimport com.github.mikephil.charting.formatter.PercentFormatter\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleFragmentDeckBattleDetailSummaryBinding\nimport com.ke.hs_tracker.module.ui.chart.PieChartData\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n/**\n * 总览\n */\n@AndroidEntryPoint\ninternal class SummaryFragment : Fragment() {\n\n    private val binding: ModuleFragmentDeckBattleDetailSummaryBinding by viewbind()\n    private val summaryViewModel: SummaryViewModel by activityViewModels()\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        launchAndRepeatWithViewLifecycle {\n            summaryViewModel.heroBattleItems.collect { list ->\n                setupPieChart(\n                    list.map {\n                        PieChartData(\n                            getString(it.first.titleRes) + it.second,\n                            it.second,\n                            it.first.color\n                        )\n                    },\n                    binding.chart\n                )\n            }\n        }\n    }\n\n    private fun setupPieChart(list: List<PieChartData>, pieChart: PieChart) {\n        pieChart.apply {\n            //设置成实心\n            holeRadius = 0f\n            transparentCircleRadius = 0f\n            //禁用描述\n            description.isEnabled = false\n            legend.isEnabled = true\n            legend.textColor = resources.getColor(R.color.module_grey500, null)\n            legend.orientation = Legend.LegendOrientation.VERTICAL\n            legend.horizontalAlignment = Legend.LegendHorizontalAlignment.LEFT\n            legend.verticalAlignment = Legend.LegendVerticalAlignment.TOP\n\n            setEntryLabelColor(resources.getColor(R.color.module_grey500, null))\n            setUsePercentValues(true)\n\n            val pieDataSet = PieDataSet(list.map {\n                return@map PieEntry(\n                    it.value.toFloat(), it.label\n                )\n            }, \"\")\n            pieDataSet.valueTextSize = 16f\n            pieDataSet.valueTextColor = resources.getColor(R.color.module_grey500, null)\n\n\n            pieDataSet.colors = list.map {\n                it.color\n            }.map {\n                resources.getColor(it, null)\n            }\n\n            val pieData = PieData(pieDataSet)\n//            pieData.setDrawValues(true)\n            pieData.setValueFormatter(PercentFormatter(this))\n\n//            pieData.setValueFormatter(object : ValueFormatter() {\n//                override fun getFormattedValue(value: Float): String {\n//                    return \"$value %\"\n//                }\n//            })\n\n            data = pieData\n            invalidate()\n        }\n    }\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/SummaryViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.deckbattledetail\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.mvvm.base.data.successOr\nimport com.ke.mvvm.base.ui.BaseViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass SummaryViewModel @Inject constructor(\n    private val getGamesByDeckCodeAndNameUseCase: GetGamesByDeckCodeAndNameUseCase,\n    savedStateHandle: SavedStateHandle\n) : BaseViewModel() {\n    private val _heroBattleItems = MutableStateFlow<List<Pair<CardClass, Int>>>(emptyList())\n\n    internal val heroBattleItems: StateFlow<List<Pair<CardClass, Int>>>\n        get() = _heroBattleItems\n\n    init {\n\n        viewModelScope.launch {\n            val code = savedStateHandle.get<String>(DeckBattleDetailActivity.EXTRA_DECK_CODE)!!\n            val name = savedStateHandle.get<String>(DeckBattleDetailActivity.EXTRA_DECK_NAME)!!\n            val list = getGamesByDeckCodeAndNameUseCase(code to name).successOr(emptyList())\n            _heroBattleItems.value = list.groupBy {\n                it.opponentHero!!\n            }.map {\n                it.key to it.value.count()\n            }\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/diagnose/DiagnoseActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.diagnose\n\nimport androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivityDiagnoseBinding\nimport com.ke.hs_tracker.module.findHSDataFilesDir\n\n/**\n * 诊断\n */\nclass DiagnoseActivity : AppCompatActivity() {\n\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n//        setContentView(R.layout.module_activity_diagnose)\n        val binding = ModuleActivityDiagnoseBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n        binding.checkConfigFile.setOnClickListener {\n            var message = \"\"\n            val logsDir = findHSDataFilesDir(\"log.config\")\n            if (logsDir == null) {\n                message = \"无法找到log.config文件\"\n            } else {\n\n            }\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/filter/FilterActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.filter\n\nimport android.content.Context\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.view.isVisible\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivityFilterBinding\nimport com.ke.hs_tracker.module.databinding.ModuleItemChipFilterBinding\nimport com.ke.hs_tracker.module.entity.Card\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.hs_tracker.module.entity.CardType\n\nclass FilterActivity : AppCompatActivity() {\n\n    private lateinit var binding: ModuleActivityFilterBinding\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityFilterBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        val cardList = intent.getParcelableArrayListExtra<Card>(EXTRA_CARD_LIST)?.toList()\n            ?: throw RuntimeException(\"cardList 不能为空\")\n\n        if (cardList.isEmpty()) {\n            finish()\n        }\n\n        binding.radioGroup.setOnCheckedChangeListener { _, id ->\n            binding.chipGroupClass.isVisible = id == R.id.rb_class\n            binding.chipGroupRarity.isVisible = id == R.id.rb_rarity\n            binding.chipGroupRace.isVisible = id == R.id.rb_minion\n            binding.chipGroupSpell.isVisible = id == R.id.rb_spell\n            binding.chipGroupWeapon.isVisible = id == R.id.rb_weapon\n            binding.chipGroupHero.isVisible = id == R.id.rb_hero\n            binding.chipGroupMechanics.isVisible = id == R.id.rb_mechanics\n        }\n\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n\n//        val cardClasses: List<Pair<CardClass, Int>> =\n        cardList.map {\n            if (it.classes.isEmpty() && it.cardClass != null) {\n                listOf(it.cardClass)\n            } else if (it.classes.isNotEmpty()) {\n                it.classes\n            } else {\n                emptyList()\n            }\n        }.filter {\n            it.isNotEmpty()\n        }.groupBy {\n            it\n        }.map {\n            it.key to it.value.size\n        }.forEach {\n\n\n            val b = ModuleItemChipFilterBinding.inflate(layoutInflater)\n            b.root.setText(\n                \"${getName(it.first, applicationContext)} ${it.second}\"\n            )\n            b.root.tag = it.first\n            binding.chipGroupClass\n                .addView(b.root)\n        }\n\n        if (binding.chipGroupClass.childCount == 0) {\n            binding.rbClass.isEnabled = false\n            binding.rbClass.isChecked = false\n        } else {\n            binding.rbClass.isChecked = true\n        }\n\n        cardList.mapNotNull {\n            it.rarity\n        }.sortedBy {\n            it.ordinal\n        }\n            .groupBy {\n                it\n            }.map {\n                it.key to it.value.size\n            }.forEach {\n                val b = ModuleItemChipFilterBinding.inflate(layoutInflater)\n                b.root.setText(\n                    \"${getString(it.first.titleRes)} ${it.second}\"\n                )\n                binding.chipGroupRarity\n                    .addView(b.root)\n            }\n\n\n\n        cardList.mapNotNull {\n            it.race\n        }.groupBy {\n            it\n        }.map {\n            it.key to it.value.size\n        }.forEach {\n            val b = ModuleItemChipFilterBinding.inflate(layoutInflater)\n            b.root.setText(\n                \"${getString(it.first.titleRes!!)} ${it.second}\"\n            )\n            binding.chipGroupRace\n                .addView(b.root)\n        }\n\n\n        if (binding.chipGroupRace.childCount == 0) {\n            //没有随从\n            binding.rbMinion.isChecked = false\n            binding.rbMinion.isEnabled = false\n        }\n\n        cardList.mapNotNull {\n            it.spellSchool\n        }.groupBy {\n            it\n        }.map {\n            it.key to it.value.size\n        }.forEach {\n            val b = ModuleItemChipFilterBinding.inflate(layoutInflater)\n            b.root.setText(\n                \"${getString(it.first.titleRes!!)} ${it.second}\"\n            )\n            binding.chipGroupSpell\n                .addView(b.root)\n        }\n\n        if (binding.chipGroupSpell.childCount == 0) {\n            binding.rbSpell.isChecked = false\n            binding.rbSpell.isEnabled = false\n        }\n\n        val weaponCount = cardList.count {\n            it.type == CardType.Weapon\n        }\n        if (weaponCount == 0) {\n            binding.rbWeapon.isChecked = false\n            binding.rbWeapon.isEnabled = false\n        } else {\n            val b = ModuleItemChipFilterBinding.inflate(layoutInflater)\n            b.root.setText(\n                \"${getString(R.string.module_weapon)} ${weaponCount}\"\n            )\n            binding.chipGroupWeapon\n                .addView(b.root)\n        }\n\n        val heroCount = cardList.count {\n            it.type == CardType.Hero\n        }\n        if (heroCount == 0) {\n            binding.rbHero.isChecked = false\n            binding.rbHero.isEnabled = false\n        } else {\n            val b = ModuleItemChipFilterBinding.inflate(layoutInflater)\n            b.root.setText(\n                \"${getString(R.string.module_hero)} ${heroCount}\"\n            )\n            binding.chipGroupHero\n                .addView(b.root)\n        }\n\n        cardList.flatMap {\n            it.mechanics\n        }.groupBy { it }\n            .map {\n                it.key to it.value.size\n            }.forEach {\n\n                val b = ModuleItemChipFilterBinding.inflate(layoutInflater)\n                b.root.setText(\n                    \"${it.first.name} ${it.second}\"\n                )\n                binding.chipGroupMechanics\n                    .addView(b.root)\n            }\n        if (binding.chipGroupMechanics.childCount == 0) {\n            binding.rbMechanics.isChecked = false\n            binding.rbMechanics.isEnabled = false\n        }\n//            cardList.flatMap {\n//                it.classes\n//            }.filter { it.display }\n//                .groupBy {\n//                    it\n//                }.map {\n//                    it.key to it.value.size\n//                }\n\n//            cardList.groupBy {\n//            it.cardClass!!\n//        }.map {\n//            it.key to it.value.count()\n//        }\n\n        //        CardClass.values()\n//            .filter {\n//                it.display\n//            }\n//            .forEach {\n//                val chip =\n//                    ModuleItemChipFilterBinding.inflate(layoutInflater).root\n//\n//                val isBlackTextColor = it.blackText\n//\n//                if (isBlackTextColor) {\n//                    chip.setTextColor(Color.BLACK)\n//                    chip.setCheckedIconResource(R.drawable.module_baseline_done_black_24dp)\n//                } else {\n//                    chip.setTextColor(Color.WHITE)\n//                    chip.setCheckedIconResource(R.drawable.module_baseline_done_white_24dp)\n//                }\n//                chip.setChipBackgroundColorResource(it.color)\n//                chip.setText(it.titleRes)\n//                binding.chipGroupClass.addView(chip)\n//            }\n//\n//        Rarity.values().forEach {\n//            val chip =\n//                ModuleItemChipFilterBinding.inflate(layoutInflater).root\n//            chip.setText(it.title)\n//            binding.chipGroupRarity.addView(chip)\n//        }\n\n    }\n\n\n    companion object {\n        const val EXTRA_CARD_LIST = \"EXTRA_CARD_LIST\"\n\n        private fun getName(list: List<CardClass>, context: Context): String {\n            if (list.isEmpty()) {\n                throw RuntimeException(\"list 不能为空\")\n            }\n            if (list.size == 1) {\n                return context.getString(list.first().titleRes)\n            }\n\n            val stringBuilder = StringBuilder()\n            stringBuilder.append(\"(\")\n\n            list.map {\n                context.getString(it.titleRes)\n            }.forEachIndexed { index, s ->\n                stringBuilder.append(s)\n                if (index != list.size - 1) {\n                    stringBuilder.append(\" \")\n                }\n            }\n            stringBuilder.append(\")\")\n\n            return stringBuilder.toString()\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/main/CardListFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.main\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.databinding.ModuleFragmentCardListBinding\nimport com.ke.hs_tracker.module.databinding.ModuleItemCardBinding\nimport com.ke.hs_tracker.module.entity.CardBean\nimport com.ke.hs_tracker.module.ui.common.CardAdapter\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.collect\n\nabstract class CardListFragment : Fragment() {\n\n    private val adapter by lazy {\n        CardAdapter()\n    }\n\n\n    private val binding: ModuleFragmentCardListBinding by viewbind()\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        binding.recyclerView.adapter = adapter\n        binding.recyclerView.addItemDecoration(\n            DividerItemDecoration(\n                requireContext(),\n                DividerItemDecoration.VERTICAL\n            )\n        )\n        launchAndRepeatWithViewLifecycle {\n            cardList.collect {\n                adapter.setList(it)\n            }\n        }\n    }\n\n\n    abstract val cardList: StateFlow<List<CardBean>>\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/main/DeckCardListFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.main\n\nimport androidx.fragment.app.activityViewModels\nimport com.ke.hs_tracker.module.entity.CardBean\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.StateFlow\n\n@AndroidEntryPoint\nclass DeckCardListFragment : CardListFragment() {\n\n    private val mainViewModel: MainViewModel by activityViewModels()\n\n    override val cardList: StateFlow<List<CardBean>>\n        get() = mainViewModel.deckLeftCardList\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/main/GraveyardFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.main\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.AdapterView\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.activityViewModels\nimport androidx.recyclerview.widget.DiffUtil\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.bindCard\nimport com.ke.hs_tracker.module.databinding.ModuleFragmentGraveyardBinding\nimport com.ke.hs_tracker.module.databinding.ModuleItemCardBinding\nimport com.ke.hs_tracker.module.entity.GraveyardCard\nimport com.ke.hs_tracker.module.showCardImageDialog\nimport com.ke.hs_tracker.module.ui.filter.FilterActivity\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n\n@AndroidEntryPoint\nclass GraveyardFragment : Fragment() {\n\n\n    private val binding: ModuleFragmentGraveyardBinding by viewbind()\n\n    private val mainViewModel: MainViewModel by activityViewModels()\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        binding.sorted.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {\n            override fun onItemSelected(p0: AdapterView<*>?, p1: View?, index: Int, p3: Long) {\n                mainViewModel.setSort(SortBy.values()[index])\n            }\n\n            override fun onNothingSelected(p0: AdapterView<*>?) {\n\n            }\n\n        }\n\n        binding.filter.setOnClickListener {\n\n            val cardList = mainViewModel.graveyardCardList.value.map {\n                it.card\n            }\n\n//                listOf(\n//                \"SCH_270\",//始生研习\n//                \"SCH_235\",//衰变飞弹\n//                \"EX1_610\",//爆炸陷阱\n//                \"TU5_CS2_029\",//火球术\n//                \"CFM_852\",//玉莲帮密探\n//                \"GIL_598\",//苔丝\n//                \"ULD_156t3\",//暴龙王\n//                \"GVG_021\",//玛尔加尼斯\n//                \"FP1_022\",//空灵召唤者\n//                \"CS2_024\",//寒冰箭\n//                \"SCH_427\",//雷霆绽放\n//                \"Story_09_BlastcrystalPotion\",//爆晶药水\n//                \"CORE_CS2_106\",//炽炎战斧\n//            )\n//                .map {\n//                    mainViewModel.allCard.find { card ->\n//                        card.id == it\n//                    }!!\n//                }\n            val intent = Intent(requireContext(), FilterActivity::class.java)\n            intent.putParcelableArrayListExtra(\n                FilterActivity.EXTRA_CARD_LIST,\n                arrayListOf<Parcelable?>().apply {\n                    addAll(cardList)\n                })\n            startActivity(intent)\n        }\n\n        binding.toggle.addOnButtonCheckedListener { _, checkedId, isChecked ->\n            if (isChecked) {\n                mainViewModel.toggleShowUserGraveyard(checkedId == R.id.self)\n            }\n        }\n\n        binding.recyclerView.adapter = adapter\n\n        adapter.setDiffCallback(object : DiffUtil.ItemCallback<GraveyardCard>() {\n            override fun areItemsTheSame(oldItem: GraveyardCard, newItem: GraveyardCard): Boolean {\n                return oldItem == newItem\n            }\n\n            override fun areContentsTheSame(\n                oldItem: GraveyardCard,\n                newItem: GraveyardCard\n            ): Boolean {\n                return oldItem == newItem\n            }\n        })\n\n        adapter.setOnItemClickListener { _, _, position ->\n\n            showCardImageDialog(requireContext(), adapter.getItem(position).card.id)\n\n        }\n\n//        launchAndRepeatWithViewLifecycle {\n//            mainViewModel.sortBy.collect { sortBy ->\n//                sortList(sortBy)\n//            }\n//        }\n\n        launchAndRepeatWithViewLifecycle {\n            mainViewModel.graveyardCardList.collect {\n                adapter.setList(it)\n                binding.filter.isEnabled = it.isNotEmpty()\n                //更新数据后重新排序下\n//                sortList(mainViewModel.sortBy.value)\n            }\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            mainViewModel.showUserGraveyardCardList.collect {\n                binding.toggle.check(if (it) R.id.self else R.id.opponent)\n            }\n        }\n\n\n    }\n\n//    private fun sortList(sortBy: SortBy) {\n//\n//\n//        binding.sorted.setSelection(SortBy.values().indexOf(sortBy))\n//        val list = adapter.data\n//        val result = when (sortBy) {\n//            SortBy.Cost -> list.sortedBy {\n//                it.card.cost\n//            }\n//            SortBy.CostReverse -> {\n//                list.sortedByDescending {\n//                    it.card.cost\n//                }\n//            }\n//            SortBy.Time -> {\n//                list.sortedBy {\n//                    it.time\n//                }\n//            }\n//            SortBy.TimeReverse -> {\n//                list.sortedByDescending { it.time }\n//            }\n//        }\n//\n//        adapter.setList(result)\n//    }\n\n    private val adapter = object : BaseViewBindingAdapter<GraveyardCard, ModuleItemCardBinding>() {\n        override fun bindItem(\n            item: GraveyardCard,\n            viewBinding: ModuleItemCardBinding,\n            viewType: Int,\n            position: Int\n        ) {\n            viewBinding.bindCard(item.card)\n        }\n\n        override fun createViewBinding(\n            inflater: LayoutInflater,\n            parent: ViewGroup,\n            viewType: Int\n        ): ModuleItemCardBinding {\n            return ModuleItemCardBinding.inflate(inflater, parent, false)\n        }\n\n    }\n\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/main/MainActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.main\n\nimport android.os.Bundle\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.fragment.app.Fragment\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport com.google.android.material.tabs.TabLayoutMediator\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivityMainBinding\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass MainActivity : AppCompatActivity() {\n\n\n//    private val adapter =\n//        object : BaseViewBindingAdapter<CardBean, ModuleItemCardBinding>() {\n//            override fun bindItem(\n//                item: CardBean,\n//                viewBinding: ModuleItemCardBinding,\n//                viewType: Int,\n//                position: Int\n//            ) {\n//                viewBinding.name.text = item.cardEntity.name + \" \" + item.count\n//                viewBinding.cost.text = item.cardEntity.cost.toString()\n//            }\n//\n//            override fun createViewBinding(\n//                inflater: LayoutInflater,\n//                parent: ViewGroup,\n//                viewType: Int\n//            ): ModuleItemCardBinding {\n//                return ModuleItemCardBinding.inflate(layoutInflater, parent, false)\n//            }\n//\n//        }\n\n    private val mainViewModel: MainViewModel by viewModels()\n    private lateinit var binding: ModuleActivityMainBinding\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityMainBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n\n\n        val titleList = listOf(\n            R.string.module_deck,\n            R.string.module_graveyard,\n            R.string.module_opponent_hand_card\n//            R.string.module_opponent_graveyard\n        )\n\n        val fragmentList = listOf(\n            DeckCardListFragment(),\n            GraveyardFragment(),\n            OpponentHandCardsFragment()\n//            UserGraveyardFragment(),\n//            OpponentGraveyardFragment()\n        )\n\n        val adapter = object : FragmentStateAdapter(this) {\n            override fun getItemCount(): Int {\n                return fragmentList.size\n            }\n\n            override fun createFragment(position: Int): Fragment {\n                return fragmentList[position]\n            }\n\n        }\n        binding.viewPager.adapter = adapter\n        binding.viewPager.offscreenPageLimit = fragmentList.size\n\n        TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, index ->\n            tab.setText(titleList[index])\n        }.attach()\n\n//        val lines = assets\n//            .open(\"Power.log\").reader()\n//            .readLines()\n//            .toMutableList().apply {\n//                removeFirstOrNull()\n//            }.toList()\n\n\n//        val mLines = mutableListOf<String>()\n//        mLines.addAll(lines)\n//\n//        val logsDir = getExternalFilesDir(\"Logs\")!!\n//        if (logsDir.exists()) {\n//            val powerFile = File(logsDir, \"Power.log\")\n//            val writer = FileOutputStream(powerFile, true).writer()\n//\n//            binding.next.setOnClickListener {\n//\n//                repeat(100) {\n//                    val line = mLines.removeFirstOrNull()\n//                    if (line != null) {\n//                        writer.append(line)\n//                        writer.appendLine()\n//                        writer.flush()\n//                    }\n//                }\n//\n//            }\n//            binding.clear.setOnClickListener {\n//                mLines.clear()\n//                mLines.addAll(lines)\n//            }\n//        }\n\n\n//        binding.recyclerView.adapter = adapter\n\n\n//        launchAndRepeatWithViewLifecycle {\n//            mainViewModel.deckLeftCardList.collect {\n//                adapter.setList(it)\n//            }\n//        }\n\n\n        launchAndRepeatWithViewLifecycle {\n            mainViewModel.title.collect {\n                binding.toolbar.title = it\n            }\n        }\n\n\n    }\n\n\n}\n\ninternal enum class SortBy {\n    Cost,\n    CostReverse,\n    Time,\n    TimeReverse\n}\n\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/main/MainViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.main\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport androidx.documentfile.provider.DocumentFile\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.db.*\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.domain.GetAllCardUseCase\nimport com.ke.hs_tracker.module.domain.GetRealLogDirUseCase\nimport com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase\nimport com.ke.hs_tracker.module.domain.SaveLogFileUseCase\nimport com.ke.hs_tracker.module.entity.*\nimport com.ke.hs_tracker.module.log\nimport com.ke.hs_tracker.module.parser.DeckFileObserver\nimport com.ke.hs_tracker.module.parser.PowerFileObserver\nimport com.ke.hs_tracker.module.parser.PowerParser\nimport com.ke.mvvm.base.data.successOr\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport java.io.InputStream\nimport java.util.*\nimport javax.inject.Inject\n\n@SuppressLint(\"StaticFieldLeak\")\n@HiltViewModel\nclass MainViewModel @Inject constructor(\n    @IoDispatcher private val dispatcher: CoroutineDispatcher,\n    @ApplicationContext private val context: Context,\n    private val parseDeckCodeUseCase: ParseDeckCodeUseCase,\n    private val getAllCardUseCase: GetAllCardUseCase,\n    private val getLogDirUseCase: GetRealLogDirUseCase,\n    private val gameDao: GameDao,\n    private val zonePositionChangedEventDao: ZonePositionChangedEventDao,\n    private val saveLogFileUseCase: SaveLogFileUseCase,\n    private val powerParser: PowerParser\n) : ViewModel() {\n    private var user: PowerTag.GameState.PlayerMapping? = null\n    private var opponent: PowerTag.GameState.PlayerMapping? = null\n\n    private var currentTurn = 0\n\n//    private var logsDir: DocumentFile? = null\n\n    private suspend fun getLogsDir(): DocumentFile? {\n        return getLogDirUseCase(Unit).successOr(null)\n    }\n\n\n    private val _title = MutableStateFlow(\"标题\")\n\n    val title: StateFlow<String>\n        get() = _title\n\n    private val _deckLeftCardList = MutableStateFlow<List<CardBean>>(emptyList())\n\n    val deckLeftCardList: StateFlow<List<CardBean>>\n        get() = _deckLeftCardList\n\n\n    private val _graveyardCardList = MutableStateFlow<List<GraveyardCard>>(emptyList())\n\n    val graveyardCardList: StateFlow<List<GraveyardCard>>\n        get() = _graveyardCardList\n\n\n    /**\n     * 对手手牌\n     */\n    private val currentOpponentHandCards = mutableListOf<OpponentHandCard>()\n\n    private val _opponentHandCards =\n        MutableStateFlow<Pair<String, List<OpponentHandCard>>>(\"\" to emptyList())\n\n    /**\n     * 对手手牌\n     */\n    val opponentHandCards: StateFlow<Pair<String, List<OpponentHandCard>>>\n        get() = _opponentHandCards\n\n    private val userGraveyardCardList = mutableListOf<GraveyardCard>()\n\n    private val opponentGraveyardCardList = mutableListOf<GraveyardCard>()\n\n\n    private val _showUserGraveyardCardList = MutableStateFlow(true)\n\n    /**\n     * 是否显示用户墓地\n     */\n    internal val showUserGraveyardCardList: StateFlow<Boolean>\n        get() = _showUserGraveyardCardList\n\n\n    internal fun toggleShowUserGraveyard(showUser: Boolean) {\n        _showUserGraveyardCardList.value = showUser\n//        _graveyardCardList.value =\n//            if (showUser) userGraveyardCardList else opponentGraveyardCardList\n        updateGraveyardCardList()\n    }\n\n\n    private val _sortBy = MutableStateFlow(SortBy.Cost)\n\n//    internal val sortBy: StateFlow<SortBy>\n//        get() = _sortBy\n\n    /**\n     * 设置排序方式\n     */\n    internal fun setSort(sortBy: SortBy) {\n        _sortBy.value = sortBy\n        updateGraveyardCardList()\n    }\n\n    private fun updateGraveyardCardList() {\n        val source =\n            if (_showUserGraveyardCardList.value) userGraveyardCardList else opponentGraveyardCardList\n\n        val result = when (_sortBy.value) {\n            SortBy.Cost -> source.sortedBy { it.card.cost }\n            SortBy.CostReverse -> source.sortedByDescending { it.card.cost }\n            SortBy.Time -> source.sortedBy { it.time }\n            SortBy.TimeReverse -> source.sortedByDescending { it.time }\n        }\n        _graveyardCardList.value = result\n    }\n\n\n    private val powerFileObserver: PowerFileObserver by lazy {\n        PowerFileObserver(1500) {\n            getFileStream(\"Power.log\")\n        }\n\n    }\n    private val deckFileObserver: DeckFileObserver by lazy {\n        DeckFileObserver {\n            getFileStream(\"Decks.log\")\n        }\n    }\n\n    private var currentDeck: CurrentDeck? = null\n    private var game: Game = Game()\n\n\n//    private val powerParser = PowerParserImpl()\n\n\n    private var allCard: List<Card> = emptyList()\n\n    private var deckCardList: List<CardBean> = emptyList()\n\n    private val zoneChangedEventList = mutableListOf<ZonePositionChangedEvent>()\n\n    private val entityIdAndCardIdMap = mutableMapOf<Int, String>()\n\n    init {\n\n        viewModelScope.launch {\n            clearPowerFile()\n            allCard = getAllCardUseCase(Unit).successOr(emptyList())\n\n        }\n\n\n        viewModelScope.launch {\n            _deckLeftCardList.collect {\n\n            }\n        }\n\n\n//        viewModelScope.launch {\n//            powerParser.powerTagFlow.collect {\n//                handlePowerTag(it)\n//            }\n//        }\n\n        powerParser.powerTagListener = {\n            handlePowerTag(it)\n        }\n        viewModelScope.launch {\n            delay(1000)\n            deckFileObserver\n                .start()\n                .flowOn(Dispatchers.IO)\n                .map {\n                    it to parseDeckCodeUseCase(it.code).successOr(emptyList())\n\n                }\n                .collect {\n                    _deckLeftCardList.value = it.second\n                    _title.value = it.first.name\n                    deckCardList = it.second\n                    currentDeck = it.first\n                }\n        }\n        viewModelScope.launch {\n            powerFileObserver.start()\n                .flowOn(dispatcher)\n                .collect {\n                    it.forEach { line ->\n                        powerParser.parse(line)\n                    }\n                }\n        }\n    }\n\n\n    private fun handlePowerTag(tag: PowerTag) {\n\n        when (tag) {\n            is PowerTag.GameState.BuildNumber -> {\n\n                game = Game(buildNumber = tag.number)\n                game.userDeckName = currentDeck?.name ?: \"\"\n                game.userDeckCode = currentDeck?.code ?: \"\"\n\n            }\n            is PowerTag.GameState.FormatType -> {\n                game.formatType = tag.type.toFormatType\n            }\n            is PowerTag.GameState.GameType -> {\n                game.gameType = tag.type.toGameType\n            }\n            is PowerTag.GameState.PlayerMapping -> {\n\n                if (tag.isUser) {\n                    game.userName = tag.name\n                    user = tag\n\n//                    game.isUserFirst = tag.first\n                } else {\n                    game.opponentName = tag.name\n                    opponent = tag\n\n                }\n\n\n            }\n            is PowerTag.GameState.ScenarioID -> {\n                game.scenarioID = tag.id.toInt()\n            }\n            is PowerTag.PowerTaskList.Block -> {\n                tag.list.forEach {\n                    handlePowerTag(it)\n                }\n\n\n            }\n            is PowerTag.PowerTaskList.CreateGame -> {\n                //初始化卡牌\n                onGameStarted()\n            }\n            is PowerTag.PowerTaskList.FullEntity -> {\n//                handleFullEntity(tag)\n                handleFullEntity(tag)\n                //FULL_ENTITY - Updating [entityName=萨尔 id=74 zone=PLAY zonePos=0 cardId=HERO_02 player=2] CardID=HERO_02\n                //    tag=CONTROLLER value=2\n                //    tag=CARDTYPE value=HERO\n                //    tag=HEALTH value=30\n                //    tag=ZONE value=PLAY\n                //    tag=ENTITY_ID value=74\n                //    tag=FACTION value=NEUTRAL\n                //    tag=RARITY value=FREE\n                //    tag=HERO_POWER value=687\n                //    tag=SPAWN_TIME_COUNT value=1\n                tag.cardId?.apply {\n                    entityIdAndCardIdMap[tag.entity.id] = this\n                }\n            }\n\n            is PowerTag.PowerTaskList.ShowEntity -> {\n                handleShowEntity(tag)\n                entityIdAndCardIdMap[tag.entity.id] = tag.cardId\n            }\n            is PowerTag.PowerTaskList.TagChange -> {\n                handleTagChange(tag)\n\n            }\n        }\n\n\n//        tag.toString().log()\n\n        (tag as? ZoneUpdatable)?.apply {\n\n\n            isUpdateZone(user?.id)?.let {\n                val cardId = it.cardId ?: entityIdAndCardIdMap[it.entityId]\n\n\n//                val event = ZonePositionChangedEvent(\n//                    entityId = it.first.id,\n//                    cardId = it.first.cardId,\n//                    isUser = it.first.player == user?.id,\n//                    currentZone = it.first.zone,\n//                    newZone = it.second,\n//                    currentPosition = it.first.zonePosition,\n//                    newPosition = it.first.zonePosition\n//                )\n                zoneChangedEventList.add(it)\n                if (it.currentZone == Zone.Deck && it.newZone != Zone.Deck && it.isUser) {\n                    //从牌库中抽取一张卡\n                    //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=48 zone=DECK zonePos=0 cardId= player=2]\n                    //    tag=CONTROLLER value=2\n                    //    tag=CARDTYPE value=SPELL\n                    //    tag=TAG_LAST_KNOWN_COST_IN_HAND value=1\n                    //    tag=COST value=1\n                    //    tag=PREMIUM value=1\n                    //    tag=ZONE value=HAND\n                    //    tag=ENTITY_ID value=48\n                    //    tag=ELITE value=1\n                    //    tag=CLASS value=SHAMAN\n                    //    tag=RARITY value=LEGENDARY\n                    //    tag=478 value=2\n                    //    tag=QUEST_PROGRESS_TOTAL value=3\n                    //    tag=676 value=1\n                    //    tag=839 value=1\n                    //    tag=1043 value=1\n                    //    tag=1068 value=0\n                    //    tag=QUEST_REWARD_DATABASE_ID value=64323\n                    //    tag=SPAWN_TIME_COUNT value=1\n\n                    //或者 探底\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=3\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=0\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1037 value=1\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE value=HAND\n                    //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=6\n\n                    //探底抽上来的卡是没有cardId的\n                    removeCardFromDeck(cardId)\n\n                } else if (it.newZone == Zone.Deck && it.currentZone != Zone.Deck && it.isUser) {\n\n                    //当心探底\n                    //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] CardID=DED_002\n                    //    tag=CONTROLLER value=1\n                    //    tag=CARDTYPE value=SPELL\n                    //    tag=TAG_LAST_KNOWN_COST_IN_HAND value=2\n                    //    tag=COST value=2\n                    //    tag=ZONE value=DECK\n                    //    tag=ENTITY_ID value=28\n                    //    tag=RARITY value=RARE\n                    //    tag=DISCOVER value=1\n                    //    tag=478 value=1\n                    //    tag=1043 value=1\n                    //    tag=1068 value=0\n                    //    tag=USE_DISCOVER_VISUALS value=1\n                    //    tag=SPAWN_TIME_COUNT value=1\n                    //    tag=SPELL_SCHOOL value=1\n                    //    tag=1711 value=1\n                    //    tag=MINI_SET value=1\n\n\n                    //有牌插入到牌库\n                    //TAG_CHANGE Entity=[entityName=冷风 id=70 zone=HAND zonePos=2 cardId=AV_266 player=2] tag=ZONE value=DECK\n\n                    //会出现id为空的情况\n                    //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=17 zone=DECK zonePos=0 cardId= player=1] CardID=\n                    //    tag=ZONE value=DECK\n                    //    tag=CONTROLLER value=1\n                    //    tag=ENTITY_ID value=17\n                    insertCardToDeck(cardId)\n\n                } else if (it.currentZone == Zone.Play && it.newZone == Zone.Graveyard) {\n                    onGraveyardCardsChanged(cardId, it.isUser)\n                } else if (it.newZone == Zone.Hand || it.currentZone == Zone.Hand) {\n                    //手牌\n                    if (!it.isUser) {\n                        handleOpponentHandChanged(it)\n\n                    }\n                }\n            }\n        }\n\n\n    }\n\n    private var lastZonePositionChangedEvent: ZonePositionChangedEvent? = null\n\n    /**\n     * 对手手牌发生变化\n     */\n    private fun handleOpponentHandChanged(event: ZonePositionChangedEvent) {\n\n        if (lastZonePositionChangedEvent?.entityId == event.entityId && event.currentZone == event.newZone) {\n            return\n        }\n\n        if (event.newZone == Zone.Hand) {\n            //有卡牌入手 可能从牌库抽到手里 也可能改变了位置\n\n            val target = currentOpponentHandCards.firstOrNull {\n                it.entityId == event.entityId\n            }\n            if (target == null) {\n                //插入一张卡牌到list中\n                currentOpponentHandCards.add(\n                    OpponentHandCard(\n                        currentTurn, event.entityId, event.newPosition\n                    )\n                )\n            } else {\n                target.position = event.newPosition\n            }\n        } else if (event.currentZone == Zone.Hand) {\n            //手牌出去了一张\n\n            val target = currentOpponentHandCards.firstOrNull {\n                it.entityId == event.entityId\n            }\n            if (target == null) {\n                //\n            } else {\n//                target.position = event.newPosition\n                currentOpponentHandCards.remove(target)\n            }\n        }\n        lastZonePositionChangedEvent = event\n        _opponentHandCards.value = UUID.randomUUID().toString() to currentOpponentHandCards\n\n        \"对手手牌发生了变化 $event ,当前回合是 $currentTurn , 对手手牌 $currentOpponentHandCards\".log()\n    }\n\n\n    private fun handleFullEntity(fullEntity: PowerTag.PowerTaskList.FullEntity) {\n        fullEntity.isUpdateHero()?.apply {\n            val cardClass = allCard.find {\n                it.id == second\n            }?.cardClass ?: return\n            if (first == user?.id) {\n                game.userHero = cardClass\n            } else if (first == opponent?.id) {\n                game.opponentHero = cardClass\n            }\n        }\n    }\n\n    private fun handleTagChange(tagChange: PowerTag.PowerTaskList.TagChange) {\n\n        tagChange.isTurnChanged()?.apply {\n            currentTurn = this\n        }\n\n        if (tagChange.entity.player == user?.id && tagChange.entity.zone == Zone.Hand && tagChange.tag == \"ZONE\" && tagChange.value == \"DECK\") {\n            //TAG_CHANGE Entity=[entityName=冷风 id=15 zone=HAND zonePos=3 cardId=AV_266 player=1] tag=ZONE value=DECK\n//            insertCardToDeck(tagChange.entity.cardId!!)\n        } else if (tagChange.tag == \"ZONE\" && tagChange.value == \"GRAVEYARD\" && tagChange.entity.zone == Zone.Play) {\n            //TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD\n            //随从死亡后进入墓地\n            //TAG_CHANGE Entity=[entityName=始生研习 id=63 zone=PLAY zonePos=0 cardId=SCH_270 player=2] tag=ZONE value=GRAVEYARD\n            //打出法术\n//            onGraveyardCardsChanged(tagChange.entity)\n\n        } else if (tagChange.isGameComplete) {\n            \"游戏结束了\".log()\n            onGameOver()\n        }\n\n        val pair = tagChange.isPlayerWonOrLost\n        if (pair != null) {\n            \"有玩家胜利或失败了 $pair $game\".log()\n            if (pair.first == game.userName) {\n                game.isUserWin = pair.second\n            } else {\n                game.opponentName = pair.first\n            }\n        }\n\n        if (tagChange.tag == \"FIRST_PLAYER\" && tagChange.value == \"1\") {\n            game.isUserFirst = tagChange.entity.entityName == game.userName\n        }\n\n    }\n\n    /**\n     * 游戏开始\n     */\n    private fun onGameStarted() {\n        \"游戏开始了\".log()\n        _deckLeftCardList.value = deckCardList\n        game.startTime = System.currentTimeMillis()\n        currentTurn = 0\n\n        currentOpponentHandCards.clear()\n        _opponentHandCards.value = UUID.randomUUID().toString() to currentOpponentHandCards\n    }\n\n    private fun onGameOver() {\n        viewModelScope.launch {\n            //保存游戏\n            game.endTime = System.currentTimeMillis()\n            gameDao.insert(game)\n            zonePositionChangedEventDao.insertAll(zoneChangedEventList.map {\n                it.cardId = entityIdAndCardIdMap[it.entityId]\n                it.gameId = game.id\n                it.cardName = allCard.find { card: Card ->\n                    it.cardId == card.id\n                }?.name\n\n                it\n            })\n            entityIdAndCardIdMap.clear()\n            zoneChangedEventList.clear()\n            getFileStream(\"Power.log\")?.apply {\n                saveLogFileUseCase(game.id to this)\n            }\n\n            delay(1000)\n            clearPowerFile()\n        }\n        _deckLeftCardList.value = deckCardList\n\n//        _userGraveyardCardList.value = emptyList()\n//        _opponentGraveyardCardList.value = emptyList()\n        userGraveyardCardList.clear()\n        opponentGraveyardCardList.clear()\n        _graveyardCardList.value = emptyList()\n        currentTurn = 0\n        _opponentHandCards.value = UUID.randomUUID().toString() to emptyList()\n    }\n\n\n    private fun handleShowEntity(showEntity: PowerTag.PowerTaskList.ShowEntity) {\n        if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads[\"ZONE\"].equals(\n                \"hand\",\n                true\n            )\n        ) {\n\n            //起始手牌\n            //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=15 zone=DECK zonePos=0 cardId= player=1] CardID=AV_266\n            //    tag=CONTROLLER value=1\n            //    tag=CARDTYPE value=SPELL\n            //    tag=TAG_LAST_KNOWN_COST_IN_HAND value=1\n            //    tag=COST value=1\n            //    tag=PREMIUM value=1\n            //    tag=ZONE value=HAND\n            //    tag=ENTITY_ID value=15\n            //    tag=RARITY value=COMMON\n            //    tag=478 value=1\n            //    tag=1043 value=1\n            //    tag=1068 value=0\n            //    tag=SPAWN_TIME_COUNT value=1\n            //    tag=SPELL_SCHOOL value=3\n//            removeCardFromDeck(showEntity.cardId)\n        } else if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads[\"ZONE\"].equals(\n                \"GRAVEYARD\",\n                true\n            )\n        ) {\n            //爆牌\n            // ShowEntity(entity=Entity(entityName=UNKNOWN ENTITY, gameCardType=Invalid, id=54, zone=Deck, zonePosition=0, cardId=null, player=2), cardId=OG_176, payloads={CONTROLLER=2, CARDTYPE=SPELL, TAG_LAST_KNOWN_COST_IN_HAND=3, COST=3, ZONE=GRAVEYARD, ENTITY_ID=54, RARITY=COMMON, 478=2, 1037=2, 1043=1, 1068=0, SPAWN_TIME_COUNT=1, SPELL_SCHOOL=6})\n//            removeCardFromDeck(showEntity.cardId)\n\n        }\n    }\n\n    private fun removeCardFromDeck(cardId: String?) {\n        onDeckCardsChanged(cardId, false)\n    }\n\n    private fun insertCardToDeck(cardId: String?) {\n        onDeckCardsChanged(cardId, true)\n    }\n\n\n    private fun onGraveyardCardsChanged(cardId: String?, isUser: Boolean) {\n\n        //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=106 zone=PLAY zonePos=0 cardId= player=1] tag=ZONE value=GRAVEYARD\n        //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=5 zone=SECRET zonePos=0 cardId= player=1] tag=COST value=2\n        //如果对面打出一张奥秘拍 会直接进入墓地\n//            ?: throw RuntimeException(\"没有id $entity\")\n        if (cardId == null) {\n            return\n        }\n        val card = findCardById(cardId)\n\n        if (card.type == CardType.Enchantment) {\n//            \"衍生牌 $card 不能放到墓地去\".log()\n            return\n        }\n\n//        \"插入一张牌到墓地 $card $entity\".log()\n\n        //TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD\n        if (isUser) {\n//            viewModelScope.launch {\n//                updateCardList(\n//                    cardEntity, _userGraveyardCardList, true\n//                )\n//            }\n            userGraveyardCardList.add(GraveyardCard(card))\n\n//            _graveyardCardList.value = userGraveyardCardList\n        } else {\n//            viewModelScope.launch {\n//                updateCardList(\n//                    cardEntity, _opponentGraveyardCardList, true\n//                )\n//            }\n            opponentGraveyardCardList.add(GraveyardCard(card))\n//            _graveyardCardList.value = opponentGraveyardCardList\n\n        }\n\n        if (isUser && _showUserGraveyardCardList.value) {\n            //是当前用户的卡插入到墓地 并且显示是当前用户的墓地\n//            _graveyardCardList.value = userGraveyardCardList\n            updateGraveyardCardList()\n        } else if (!isUser && _showUserGraveyardCardList.value) {\n//            _graveyardCardList.value = opponentGraveyardCardList\n            updateGraveyardCardList()\n        }\n    }\n\n    private fun onDeckCardsChanged(cardId: String?, insert: Boolean) {\n\n        if (cardId == null) {\n            return\n        }\n\n        val card = findCardById(cardId)\n\n        updateCardList(card, _deckLeftCardList, insert)\n\n    }\n\n    private fun findCardById(cardId: String) =\n        allCard.find { it.id == cardId } ?: throw RuntimeException(\"id为 $cardId 没有这张牌\")\n\n\n    private suspend fun clearPowerFile() {\n\n        withContext(dispatcher) {\n            val documentFile = getLogsDir()?.findFile(powerFileName)\n            documentFile?.apply {\n                context.contentResolver.openOutputStream(uri, \"wt\")?.use {\n                    it.write(\"\".encodeToByteArray())\n                    it.flush()\n                    it.close()\n                }\n            }\n\n            powerFileObserver.reset()\n            deckFileObserver.reset()\n        }\n\n\n    }\n\n    private suspend fun getFileStream(fileName: String): InputStream? = withContext(dispatcher) {\n        val documentFile = getLogsDir()?.findFile(fileName)\n        if (documentFile == null) {\n            \"无法访问 $fileName 文件\".log()\n\n            return@withContext null\n        }\n\n        context.contentResolver.openInputStream(documentFile.uri)\n    }\n\n    companion object {\n        fun updateCardList(\n            card: Card,\n            mutableStateFlow: MutableStateFlow<List<CardBean>>,\n            insert: Boolean,\n        ) {\n            if (card.type == CardType.Enchantment) {\n                return\n            }\n            val list = mutableStateFlow.value.toMutableList()\n            val bean = list.find {\n                it.card.id == card.id\n            }\n\n            if (bean == null) {\n                list.add(CardBean(card, 1))\n            } else {\n                val newCount = if (!insert) bean.count - 1 else bean.count + 1\n                list[list.indexOf(bean)] = bean.updateCount(newCount)\n            }\n\n            if (bean?.count == 3) {\n                \"插入了3张进去？ \".log()\n            }\n\n            mutableStateFlow.value = list.sortedBy {\n                it.card.cost\n            }.filter {\n                it.count != 0\n            }\n        }\n    }\n}\n\nconst val powerFileName = \"Power.log\"\n\ndata class OpponentHandCard(\n    /**\n     * 回合数\n     */\n    val turn: Int,\n    /**\n     * 实体id\n     */\n    val entityId: Int,\n    /**\n     * 位置\n     */\n    var position: Int,\n\n    /**\n     * 时间\n     */\n    val time: Long = System.currentTimeMillis()\n)"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/main/OpponentGraveyardFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.main\n\nimport androidx.fragment.app.activityViewModels\nimport com.ke.hs_tracker.module.entity.CardBean\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.StateFlow\n\n//@AndroidEntryPoint\n//class OpponentGraveyardFragment : CardListFragment() {\n//\n//    private val mainViewModel: MainViewModel by activityViewModels()\n//\n//    override val cardList: StateFlow<List<CardBean>>\n//        get() = mainViewModel.opponentGraveyardCardList\n//}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/main/OpponentHandCardsFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.main\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.activityViewModels\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.databinding.ModuleFragmentOpponentHandCardsBinding\nimport com.ke.hs_tracker.module.databinding.ModuleItemOpponentHandCardBinding\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass OpponentHandCardsFragment : Fragment() {\n    private val mainViewModel: MainViewModel by activityViewModels()\n    private val binding: ModuleFragmentOpponentHandCardsBinding by viewbind()\n\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ) = binding.root\n\n    private val adapter =\n        object : BaseViewBindingAdapter<OpponentHandCard, ModuleItemOpponentHandCardBinding>() {\n            override fun bindItem(\n                item: OpponentHandCard,\n                viewBinding: ModuleItemOpponentHandCardBinding,\n                viewType: Int,\n                position: Int\n            ) {\n                viewBinding.text.text = (item.turn).toString()\n            }\n\n            override fun createViewBinding(\n                inflater: LayoutInflater,\n                parent: ViewGroup,\n                viewType: Int\n            ): ModuleItemOpponentHandCardBinding {\n                return ModuleItemOpponentHandCardBinding.inflate(inflater, parent, false)\n            }\n\n        }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        binding.recyclerView.adapter = adapter\n\n        launchAndRepeatWithViewLifecycle {\n            mainViewModel.opponentHandCards.collect {\n                adapter.setList(\n                    it.second\n                        .sortedBy { card ->\n                            card.position\n                        }\n                        .sortedBy { card ->\n                            card.time\n                        }\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/main/UserGraveyardFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.main\n\nimport androidx.fragment.app.activityViewModels\nimport com.ke.hs_tracker.module.entity.CardBean\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.StateFlow\n\n//@AndroidEntryPoint\n//class UserGraveyardFragment : CardListFragment() {\n//\n//    private val mainViewModel: MainViewModel by activityViewModels()\n//\n//    override val cardList: StateFlow<List<CardBean>>\n//        get() = mainViewModel.userGraveyardCardList\n//}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/migrate/MigrateDataConvert.kt",
    "content": "package com.ke.hs_tracker.module.ui.migrate\n\nimport androidx.annotation.WorkerThread\nimport com.ke.hs_tracker.module.db.*\nimport com.squareup.moshi.JsonClass\nimport com.squareup.moshi.Moshi\nimport javax.inject.Inject\n\ninternal class MigrateDataConvert @Inject constructor(\n    private val gameDao: GameDao,\n    private val zonePositionChangedEventDao: ZonePositionChangedEventDao,\n    private val moshi: Moshi\n) {\n\n    private val adapter = moshi.adapter(MigrateData::class.java)\n\n    @WorkerThread\n    suspend fun getJsonString(): String {\n\n        val migrateData =\n            MigrateData(DATABASE_VERSION, gameDao.getAll(), zonePositionChangedEventDao.getAll())\n\n        return adapter.toJson(migrateData)\n\n    }\n\n    suspend fun save(jsonString: String): Boolean {\n        val data = adapter.fromJson(jsonString) ?: return false\n        saveData(data)\n        return true\n    }\n\n    @WorkerThread\n    private suspend fun saveData(migrateData: MigrateData) {\n        gameDao.insert(migrateData.games)\n        zonePositionChangedEventDao.insertAll(migrateData.zonePositionChangedEvents)\n    }\n}\n\n@JsonClass(generateAdapter = true)\ninternal data class MigrateData(\n    val version: Int,\n    val games: List<Game>,\n    val zonePositionChangedEvents: List<ZonePositionChangedEvent>\n)"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/migrate/MigrateMainActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.migrate\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.wifi.WifiManager\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport com.ke.hs_tracker.module.databinding.ModuleActivityMigrateMainBinding\n\nclass MigrateMainActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val binding = ModuleActivityMigrateMainBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n\n        binding.toThisPhone.setOnClickListener {\n            startActivity(Intent(this, SocketServerActivity::class.java))\n            finish()\n        }\n        binding.toAnotherPhone.setOnClickListener {\n            startActivity(Intent(this, SocketClientActivity::class.java))\n            finish()\n        }\n\n    }\n}\n\ninternal fun getLocalIPAddress(context: Context): String {\n\n    val manager =\n        context.applicationContext.getSystemService(AppCompatActivity.WIFI_SERVICE) as WifiManager\n    return int2ip(manager.connectionInfo.ipAddress)\n}\n\n\ninternal fun int2ip(ipInt: Int): String {\n    val sb = StringBuilder()\n    sb.append(ipInt and 0xFF).append(\".\")\n    sb.append(ipInt shr 8 and 0xFF).append(\".\")\n    sb.append(ipInt shr 16 and 0xFF).append(\".\")\n    sb.append(ipInt shr 24 and 0xFF)\n    return sb.toString()\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/migrate/SocketClientActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.migrate\n\nimport android.app.ProgressDialog\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport com.ke.hs_tracker.module.databinding.ModuleActivitySocketClientBinding\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.net.Socket\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass SocketClientActivity : AppCompatActivity() {\n\n    @Inject\n    internal lateinit var migrateDataConvert: MigrateDataConvert\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n//        setContentView(R.layout.module_activity_socket_client)\n        val binding: ModuleActivitySocketClientBinding =\n            ModuleActivitySocketClientBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n//        binding.ipAddress.setText(getLocalIPAddress(applicationContext))\n\n\n        binding.start.setOnClickListener {\n\n            val progressDialog = ProgressDialog(this)\n            progressDialog.show()\n\n            lifecycleScope.launch(Dispatchers.IO) {\n                val host = binding.ipAddress.text?.toString() ?: return@launch\n                val port = binding.ipPort.text?.toString()?.toInt() ?: return@launch\n\n                try {\n                    val socket = Socket(host, port)\n                    val text = migrateDataConvert.getJsonString()\n\n\n                    socket.getOutputStream().apply {\n                        write(\n                            text.toByteArray()\n                        )\n                        flush()\n                        close()\n                    }\n                    runOnUiThread {\n                        progressDialog.dismiss()\n                        finish()\n                    }\n\n                } catch (e: Exception) {\n                    runOnUiThread {\n                        progressDialog.dismiss()\n                    }\n                    e.printStackTrace()\n                }\n\n\n            }\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/migrate/SocketServerActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.migrate\n\nimport android.app.ProgressDialog\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.view.isVisible\nimport androidx.lifecycle.lifecycleScope\nimport com.ke.hs_tracker.module.databinding.ModuleActivitySocketServerBinding\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.net.ServerSocket\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass SocketServerActivity : AppCompatActivity() {\n\n\n    @Inject\n    internal lateinit var migrateDataConvert: MigrateDataConvert\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n//        setContentView(R.layout.module_activity_socket_server)\n        val binding = ModuleActivitySocketServerBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n        binding.ipAddress.setText(getLocalIPAddress(applicationContext))\n\n\n        binding.start.setOnClickListener {\n            val port = binding.ipPort.text.toString().toIntOrNull() ?: return@setOnClickListener\n            it.isEnabled = false\n            binding.loadingProgress.isVisible = true\n            start(port)\n        }\n    }\n\n    private fun start(port: Int) {\n        val progressDialog = ProgressDialog(this)\n        progressDialog.show()\n\n        lifecycleScope.launch {\n            withContext(Dispatchers.IO) {\n                val serverSocket = ServerSocket(port)\n                val socket = serverSocket.accept()\n                val text = socket.getInputStream().bufferedReader().readLine() ?: \"\"\n//                Logger.d(\"收到了数据 $text\")\n                migrateDataConvert.save(text)\n                runOnUiThread {\n                    finish()\n                }\n            }\n\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/permissions/PermissionsActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.permissions\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.provider.DocumentsContract\nimport android.view.View\nimport androidx.activity.result.contract.ActivityResultContract\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.documentfile.provider.DocumentFile\nimport com.ke.hs_tracker.module.HS_DATA_FILE_DIR\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.canReadDataDir\nimport com.ke.hs_tracker.module.databinding.ModuleActivityPermissionsBinding\nimport com.ke.hs_tracker.module.hasAllPermissions\nimport com.ke.hs_tracker.module.ui.writeconfig.WriteConfigActivity\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass PermissionsActivity : AppCompatActivity() {\n    private lateinit var binding: ModuleActivityPermissionsBinding\n    private val viewModel: PermissionsViewModel by viewModels()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityPermissionsBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        setTitle(R.string.module_set_permissions)\n\n\n        initView()\n\n        launchAndRepeatWithViewLifecycle {\n            viewModel.navigationActions.collect {\n//                val intent = when (it) {\n//                    PermissionsNavigationAction.NavigateToMain -> {\n//                        Intent(this@PermissionsActivity, MainActivity::class.java)\n//                    }\n//                    PermissionsNavigationAction.NavigateToSync -> {\n//                        Intent(this@PermissionsActivity, SyncCardDataActivity::class.java)\n//                    }\n//                }\n                startActivity(Intent(this@PermissionsActivity, WriteConfigActivity::class.java))\n                finish()\n            }\n        }\n    }\n\n    private fun initView() {\n//        val requestPermissionLauncher =\n//            registerForActivityResult(ActivityResultContracts.RequestPermission()) {\n//\n//            }\n\n        val requestAccessDataDirLauncher = registerForActivityResult(RequestAccessDataDir()) {\n            if (it != null) {\n                contentResolver.takePersistableUriPermission(\n                    it,\n                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION\n                )\n            }\n        }\n\n//        binding.step3.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R\n\n//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n//            val launcher = registerForActivityResult(RequestManageAllFilesAccessPermission()) {\n//\n//            }\n//            binding.step3.setOnClickListener {\n//                launcher.launch(Unit)\n//            }\n//        }\n\n        val onClickListener = View.OnClickListener {\n            when (it) {\n//                binding.step1 -> {\n//                    requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)\n//                }\n                binding.step2 -> {\n                    requestAccessDataDirLauncher.launch(Unit)\n                }\n                binding.next -> {\n\n                    viewModel.next()\n\n                }\n            }\n        }\n\n//        binding.step1.setOnClickListener(onClickListener)\n        binding.step2.setOnClickListener(onClickListener)\n        binding.next.setOnClickListener(onClickListener)\n    }\n\n    override fun onResume() {\n        super.onResume()\n\n        binding.next.isEnabled = hasAllPermissions\n\n//        val hasPermission =\n//            checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED\n//        if (hasPermission) {\n//            binding.step1.isEnabled = false\n//            binding.step1.setCompoundDrawablesRelativeWithIntrinsicBounds(\n//                0,\n//                0,\n//                R.drawable.module_baseline_done_green_500_24dp,\n//                0\n//            )\n//        } else {\n//            binding.step1.isEnabled = true\n//            binding.step1.setCompoundDrawablesRelativeWithIntrinsicBounds(\n//                0,\n//                0,\n//                R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp,\n//                0\n//            )\n//        }\n\n        if (canReadDataDir) {\n            binding.step2.isEnabled = false\n            binding.step2.setCompoundDrawablesRelativeWithIntrinsicBounds(\n                0,\n                0,\n                R.drawable.module_baseline_done_green_500_24dp,\n                0\n            )\n        } else {\n            binding.step2.isEnabled = true\n            binding.step2.setCompoundDrawablesRelativeWithIntrinsicBounds(\n                0,\n                0,\n                R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp,\n                0\n            )\n        }\n\n//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n//            if (isExternalStorageManager()) {\n//                binding.step3.isEnabled = false\n//                binding.step3.setCompoundDrawablesRelativeWithIntrinsicBounds(\n//                    0,\n//                    0,\n//                    R.drawable.module_baseline_done_green_500_24dp,\n//                    0\n//                )\n//            } else {\n//                binding.step3.isEnabled = true\n//                binding.step3.setCompoundDrawablesRelativeWithIntrinsicBounds(\n//                    0,\n//                    0,\n//                    R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp,\n//                    0\n//                )\n//            }\n//        }\n    }\n\n\n}\n\nclass RequestAccessDataDir : ActivityResultContract<Unit, Uri?>() {\n    override fun createIntent(context: Context, input: Unit): Intent {\n        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)\n        intent.flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION\n                or Intent.FLAG_GRANT_WRITE_URI_PERMISSION\n                or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION\n                or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)\n        val documentFile = DocumentFile.fromTreeUri(context.applicationContext, HS_DATA_FILE_DIR)!!\n        intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.uri)\n\n        return intent\n    }\n\n    override fun parseResult(resultCode: Int, intent: Intent?): Uri? {\n        return intent?.data\n    }\n\n}\n\n//@RequiresApi(Build.VERSION_CODES.R)\n//class RequestManageAllFilesAccessPermission : ActivityResultContract<Unit, Boolean>() {\n//    override fun createIntent(context: Context, input: Unit): Intent {\n//        val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)\n//        intent.data = Uri.parse(\"package:${context.packageName}\")\n//        return intent\n//    }\n//\n//    override fun parseResult(resultCode: Int, intent: Intent?): Boolean {\n//        return Environment.isExternalStorageManager()\n//    }\n//\n//}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/permissions/PermissionsViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.permissions\n\nimport android.content.Context\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.domain.GetDatabaseCardCountUseCase\nimport com.ke.hs_tracker.module.domain.WriteLogConfigFileUseCase\nimport com.ke.mvvm.base.data.successOr\nimport com.ke.mvvm.base.ui.BaseViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass PermissionsViewModel @Inject constructor(\n    private val getDatabaseCardCountUseCase: GetDatabaseCardCountUseCase,\n    private val writeLogConfigFileUseCase: WriteLogConfigFileUseCase\n) : BaseViewModel() {\n\n    private val _navigationActions =\n        Channel<PermissionsNavigationAction>(capacity = Channel.CONFLATED)\n\n    val navigationActions: Flow<PermissionsNavigationAction>\n        get() = _navigationActions.receiveAsFlow()\n\n    internal fun next() {\n\n        viewModelScope.launch {\n            _navigationActions.send(PermissionsNavigationAction.NavigateToWriteConfig)\n//            writeLogConfigFileUseCase(true)\n//            if (getDatabaseCardCountUseCase(Unit).successOr(0) == 0) {\n//                _navigationActions.send(PermissionsNavigationAction.NavigateToSync)\n//            } else {\n//                _navigationActions.send(PermissionsNavigationAction.NavigateToMain)\n//            }\n        }\n    }\n}\n\nsealed interface PermissionsNavigationAction {\n//    data class ShowErrorDialog(\n//        val message: String\n//    ) : PermissionsNavigationAction\n\n//    object NavigateToMain : PermissionsNavigationAction\n\n    object NavigateToWriteConfig : PermissionsNavigationAction\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordAdapter.kt",
    "content": "package com.ke.hs_tracker.module.ui.records\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleItemRecordBinding\nimport com.ke.hs_tracker.module.db.Game\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\nimport java.text.SimpleDateFormat\nimport java.util.*\n\nclass RecordAdapter : BaseViewBindingAdapter<Game, ModuleItemRecordBinding>() {\n\n\n    override fun bindItem(\n        item: Game,\n        viewBinding: ModuleItemRecordBinding,\n        viewType: Int,\n        position: Int\n    ) {\n        viewBinding.apply {\n            userHero.setImageResource(item.userHero!!.roundIcon!!)\n            opponentHero.setImageResource(item.opponentHero!!.roundIcon!!)\n            date.text = simpleDateFormat.format(Date(item.startTime))\n            state.isEnabled = item.isUserWin ?: true\n            val type =\n                root.context.getString(item.formatType.title) + root.context.getString(item.gameType.title)\n            gameType.text = type\n            state.setText(if (item.isUserWin == true) R.string.module_win else R.string.module_loss)\n        }\n    }\n\n    override fun createViewBinding(\n        inflater: LayoutInflater,\n        parent: ViewGroup,\n        viewType: Int\n    ): ModuleItemRecordBinding {\n        return ModuleItemRecordBinding.inflate(inflater, parent, false)\n    }\n}\n\nprivate val simpleDateFormat = SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\")"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordsActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.records\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport com.ke.hs_tracker.module.databinding.ModuleActivityRecordsBinding\nimport com.ke.hs_tracker.module.ui.zoneevents.ZoneEventsActivity\nimport com.ke.mvvm.base.data.ViewStatus\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass RecordsActivity : AppCompatActivity() {\n\n    private val adapter by lazy {\n       RecordAdapter().apply {\n\n\n           setOnItemClickListener { _, _, position ->\n               startActivity(Intent(this@RecordsActivity, ZoneEventsActivity::class.java).apply {\n                   putExtra(ZoneEventsActivity.EXTRA_KEY_ID, getItem(position).id)\n               })\n           }\n       }\n    }\n\n    private val recordsViewModel: RecordsViewModel by viewModels()\n    private lateinit var binding: ModuleActivityRecordsBinding\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityRecordsBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        binding.recyclerView.adapter = adapter\n        binding.recyclerView.addItemDecoration(\n            DividerItemDecoration(\n                this,\n                DividerItemDecoration.VERTICAL\n            )\n        )\n\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n\n\n//        binding.toolbar.menu.apply {\n//            clear()\n//            add(\n//                0,\n//                0,\n//                0,\n//                R.string.module_settings\n//            ).setIcon(R.drawable.module_baseline_settings_white_24dp)\n//                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)\n//\n//\n//        }\n//        binding.toolbar.setOnMenuItemClickListener {\n//            startActivity(Intent(this, SettingsActivity::class.java))\n//            true\n//        }\n        binding.swipeRefreshLayout.setOnRefreshListener {\n            recordsViewModel.loadData()\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            recordsViewModel.viewStatus.collect {\n                binding.swipeRefreshLayout.isRefreshing = it is ViewStatus.Loading\n                when (it) {\n                    is ViewStatus.Loading -> {\n\n                    }\n                    is ViewStatus.Content -> {\n                        adapter.setList(it.data)\n                    }\n                    is ViewStatus.Error -> {\n\n                    }\n                }\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordsViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.records\n\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.db.Game\nimport com.ke.hs_tracker.module.db.GameDao\nimport com.ke.mvvm.base.ui.BaseContentViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass RecordsViewModel @Inject constructor(private val gameDao: GameDao) :\n    BaseContentViewModel<List<Game>>() {\n\n\n    init {\n        loadData()\n    }\n\n    internal fun loadData() {\n\n        viewModelScope.launch {\n            showLoading()\n            showContent(gameDao.getAll().filter {\n                //掉线问题\n                it.opponentHero != null && it.isUserFirst != null\n            }.sortedByDescending {\n                it.endTime\n            })\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/settings/SettingsActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.settings\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.widget.CompoundButton\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.data.PreferenceStorage\nimport com.ke.hs_tracker.module.databinding.ModuleActivitySettingsBinding\nimport com.ke.hs_tracker.module.ui.deck.DeckCodeParserActivity\nimport com.ke.hs_tracker.module.ui.migrate.MigrateMainActivity\nimport com.ke.hs_tracker.module.ui.support.SupportActivity\nimport com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity\nimport com.ke.hs_tracker.module.ui.test.TestActivity\nimport com.ke.hs_tracker.module.ui.theme.ThemeActivity\nimport com.ke.hs_tracker.module.ui.writeconfig.WriteConfigActivity\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass SettingsActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener {\n    private lateinit var binding: ModuleActivitySettingsBinding\n    private val settingsViewModel: SettingsViewModel by viewModels()\n\n    @Inject\n    lateinit var preferenceStorage: PreferenceStorage\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivitySettingsBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        binding.floating.isChecked = preferenceStorage.floatingEnable\n\n        binding.floating.setOnCheckedChangeListener { _, isChecked ->\n            preferenceStorage.floatingEnable = isChecked\n        }\n\n\n        launchAndRepeatWithViewLifecycle {\n            settingsViewModel.saveLogFileEnable.collect {\n                binding.saveLogFile.setOnCheckedChangeListener(null)\n                binding.saveLogFile.isChecked = it\n                binding.saveLogFile.setOnCheckedChangeListener(this@SettingsActivity)\n            }\n        }\n\n        binding.toolbar.apply {\n            setNavigationOnClickListener {\n                onBackPressed()\n            }\n            this.menu.add(0, 0, 0, \"测试\")\n            this.setOnMenuItemClickListener {\n                startActivity(Intent(this@SettingsActivity, TestActivity::class.java))\n\n                true\n            }\n        }\n\n\n        binding.theme.setOnClickListener {\n            startActivity(Intent(this, ThemeActivity::class.java))\n        }\n\n        binding.codeParser.setOnClickListener {\n            startActivity(Intent(this, DeckCodeParserActivity::class.java))\n\n        }\n\n        binding.migrate.setOnClickListener {\n            startActivity(Intent(this, MigrateMainActivity::class.java))\n        }\n\n        binding.rewriteConfigFile.setOnClickListener {\n            val intent = Intent(this, WriteConfigActivity::class.java)\n            intent.putExtra(WriteConfigActivity.EXTRA_REWRITE, true)\n            startActivity(intent)\n        }\n\n        binding.sync.setOnClickListener {\n            val intent = Intent(this, SyncCardDataActivity::class.java)\n            intent.putExtra(SyncCardDataActivity.EXTRA_SHOW_BACK_BUTTON, true)\n            startActivity(intent)\n        }\n\n        //给作者发邮件\n        binding.llContactAuthor.setOnClickListener {\n            val email = getString(R.string.module_author_email)\n            val intent = Intent(Intent.ACTION_SEND)\n            intent.type = \"text/plain\"\n            intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))\n            startActivity(Intent.createChooser(intent, \"Send To\"))\n        }\n\n        binding.llSource.setOnClickListener {\n            val intent = Intent(Intent.ACTION_VIEW)\n            intent.data = Uri.parse(getString(R.string.module_github_address))\n            startActivity(intent)\n        }\n\n        binding.support.setOnClickListener {\n            startActivity(Intent(this, SupportActivity::class.java))\n        }\n    }\n\n    override fun onCheckedChanged(p0: CompoundButton, checked: Boolean) {\n        settingsViewModel.setSaveLogFileEnable(checked)\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/settings/SettingsViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.settings\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.domain.GetSaveLogFileEnableUseCase\nimport com.ke.hs_tracker.module.domain.SetSaveLogFileEnableUseCase\nimport com.ke.mvvm.base.data.successOr\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n    private val getSaveLogFileEnableUseCase: GetSaveLogFileEnableUseCase,\n    private val setSaveLogFileEnableUseCase: SetSaveLogFileEnableUseCase\n) : ViewModel() {\n\n\n    private val _saveLogFileEnable = MutableStateFlow(false)\n    internal val saveLogFileEnable: StateFlow<Boolean>\n        get() = _saveLogFileEnable\n\n    init {\n\n        viewModelScope.launch {\n            _saveLogFileEnable.value = getSaveLogFileEnableUseCase(Unit).successOr(false)\n        }\n    }\n\n    internal fun setSaveLogFileEnable(enable: Boolean) {\n        viewModelScope.launch {\n            _saveLogFileEnable.value = setSaveLogFileEnableUseCase(enable).successOr(enable)\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/splash/SplashActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.splash\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AppCompatActivity\nimport com.ke.hs_tracker.module.ui.permissions.PermissionsActivity\nimport com.ke.hs_tracker.module.ui.summary.SummaryActivity\nimport com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n\n@AndroidEntryPoint\n@SuppressLint(\"CustomSplashScreen\")\nclass SplashActivity : AppCompatActivity() {\n\n    private val splashViewModel: SplashViewModel by viewModels()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n//        val intent = if (hasAllPermissions) {\n//            Intent(this, MainActivity::class.java)\n//        } else {\n//            Intent(this, PermissionsActivity::class.java)\n//        }\n//        startActivity(intent)\n\n        launchAndRepeatWithViewLifecycle {\n\n//            \"写入log文件结果 ${writeLogFile()}\".log()\n\n//            val result = writeLogFile()\n//\n//            \"写入文件结果 $result\".log()\n\n\n            splashViewModel.navigationActions.collect {\n                val clazz = when (it) {\n                    SplashNavigationAction.NavigateToMain -> SummaryActivity::class.java\n                    SplashNavigationAction.NavigateToPermissions -> PermissionsActivity::class.java\n                    SplashNavigationAction.NavigateToSync -> SyncCardDataActivity::class.java\n                }\n                val intent = Intent(this@SplashActivity, clazz)\n                startActivity(intent)\n            }\n        }\n    }\n\n\n//    private suspend fun writeLogFile(): Boolean {\n//        return withContext(Dispatchers.IO) {\n//\n//\n//            val documentFile = findHSDataFilesDir(\"Logs\") ?: return@withContext false\n//\n//            val fileName = \"Power.log\"\n//            documentFile?.findFile(fileName)?.delete()\n//            val configFile = documentFile.createFile(\"plain/text\", fileName)\n//                ?: return@withContext false\n//\n//            contentResolver.openOutputStream(configFile.uri)?.use {\n//                assets.open(\"Power.log\")\n//                    .copyTo(it)\n//                it.flush()\n//            }\n//\n//            return@withContext true\n//        }\n//\n//\n//    }\n\n    override fun onStop() {\n        super.onStop()\n        finish()\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/splash/SplashViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.splash\n\nimport android.content.Context\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.domain.GetDatabaseCardCountUseCase\nimport com.ke.hs_tracker.module.hasAllPermissions\nimport com.ke.mvvm.base.data.successOr\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass SplashViewModel @Inject constructor(\n    @ApplicationContext context: Context,\n    private val getDatabaseCardCountUseCase: GetDatabaseCardCountUseCase\n) :\n    ViewModel() {\n\n    private val _navigationActions = Channel<SplashNavigationAction>(capacity = Channel.CONFLATED)\n\n    val navigationActions: Flow<SplashNavigationAction>\n        get() = _navigationActions.receiveAsFlow()\n\n    init {\n        viewModelScope.launch {\n            if (context.hasAllPermissions) {\n\n                if (getDatabaseCardCountUseCase(Unit).successOr(0) == 0) {\n                    _navigationActions.send(SplashNavigationAction.NavigateToSync)\n                } else {\n                    _navigationActions.send(SplashNavigationAction.NavigateToMain)\n                }\n            } else {\n                _navigationActions.send(SplashNavigationAction.NavigateToPermissions)\n            }\n        }\n\n\n    }\n}\n\nsealed interface SplashNavigationAction {\n    object NavigateToPermissions : SplashNavigationAction\n\n    object NavigateToMain : SplashNavigationAction\n\n    object NavigateToSync : SplashNavigationAction\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateItem.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport androidx.annotation.IntRange\nimport com.ke.hs_tracker.module.entity.CardClass\n\n/**\n * 对局胜率\n */\nsealed interface BattleRateItem {\n    /**\n     * 胜率局数\n     */\n    val winCount: Int\n\n    /**\n     * 失败局数\n     */\n    val lostCount: Int\n\n    /**\n     * 总局数\n     */\n    val allCount: Int\n\n    /**\n     * 先手局数\n     */\n    val firstHandCount: Int\n\n    /**\n     * 胜率\n     */\n    @get:IntRange(from = 0, to = 100)\n    val rate: Int\n\n    /**\n     * 职业对战胜率\n     */\n    data class ClassBattleRate(\n        override val winCount: Int,\n        override val lostCount: Int,\n        override val firstHandCount: Int,\n        val cardClass: CardClass\n    ) : BattleRateItem {\n        override val allCount: Int\n            get() = winCount + lostCount\n\n        override val rate: Int\n            get() = if (allCount == 0) 0 else winCount * 100 / allCount\n    }\n\n    /**\n     * 卡组对战胜率\n     */\n    data class DeckBattleRate(\n        override val winCount: Int,\n        override val lostCount: Int,\n        override val firstHandCount: Int,\n        val deckName: String,\n        val deckCode: String\n    ) : BattleRateItem {\n\n        override val allCount: Int\n            get() = winCount + lostCount\n\n        override val rate: Int\n            get() = winCount * 100 / allCount\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateItemAdapter.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleItemSummaryBattleBinding\nimport com.ke.hs_tracker.module.ui.classbattledetail.ClassBattleDetailActivity\nimport com.ke.hs_tracker.module.ui.deckbattledetail.DeckBattleDetailActivity\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\n\ninternal class BattleRateItemAdapter :\n    BaseViewBindingAdapter<BattleRateItem, ModuleItemSummaryBattleBinding>() {\n    override fun bindItem(\n        item: BattleRateItem,\n        viewBinding: ModuleItemSummaryBattleBinding,\n        viewType: Int,\n        position: Int\n    ) {\n        viewBinding.apply {\n            allCount.text = \"总：\" + (item.lostCount + item.winCount).toString()\n            winCount.text = \"胜：\" + item.winCount.toString()\n            lostCount.text = \"负：\" + item.lostCount.toString()\n\n            winRate.text = item.rate.toString() + \"%\"\n            when (item) {\n                is BattleRateItem.ClassBattleRate -> {\n                    image.setImageResource(item.cardClass.roundIcon!!)\n                    name.setText(item.cardClass.titleRes)\n                    root.setOnClickListener {\n                        it.context.startActivity(\n                            ClassBattleDetailActivity.createIntent(\n                                it.context,\n                                item.cardClass\n                            )\n                        )\n                    }\n                }\n                is BattleRateItem.DeckBattleRate -> {\n                    image.setImageResource(R.drawable.module_image_round_demon_hunter)\n                    name.text = item.deckName\n                    root.setOnClickListener {\n                        it.context.startActivity(\n                            DeckBattleDetailActivity.createIntent(\n                                it.context,\n                                item.deckCode,\n                                item.deckName\n                            )\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    override fun createViewBinding(\n        inflater: LayoutInflater,\n        parent: ViewGroup,\n        viewType: Int\n    ): ModuleItemSummaryBattleBinding {\n        return ModuleItemSummaryBattleBinding.inflate(inflater, parent, false)\n    }\n\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateListFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.R\nimport com.ke.mvvm.base.data.ViewStatus\nimport com.ke.mvvm.base.databinding.KeMvvmLayoutBaseRefreshListRetryBinding\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport kotlinx.coroutines.flow.collect\n\nabstract class BattleRateListFragment : Fragment() {\n\n    internal val adapter = BattleRateItemAdapter()\n\n    private val binding: KeMvvmLayoutBaseRefreshListRetryBinding by viewbind()\n\n    protected abstract val viewModel: BattleRateListViewModel<*>\n\n    final override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        lifecycle.addObserver(viewModel)\n        binding.recyclerView.adapter = adapter\n        binding.recyclerView.addItemDecoration(\n            DividerItemDecoration(\n                requireContext(),\n                DividerItemDecoration.VERTICAL\n            )\n        )\n        adapter.addFooterView(\n            layoutInflater.inflate(R.layout.module_item_footer_with_fab, null)\n        )\n\n        binding.swipeRefreshLayout.setOnRefreshListener {\n            viewModel.refresh()\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            viewModel.viewStatus.collect {\n                when (it) {\n                    is ViewStatus.Loading -> {\n                        binding.swipeRefreshLayout.isRefreshing = true\n                    }\n                    is ViewStatus.Content -> {\n                        binding.swipeRefreshLayout.isRefreshing = false\n                        adapter.setList(it.data.sortedByDescending { item ->\n                            item.rate\n                        })\n                    }\n                    is ViewStatus.Error -> throw IllegalArgumentException(\"不该出现错误的情况\")\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateListViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport androidx.lifecycle.DefaultLifecycleObserver\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.viewModelScope\nimport com.ke.mvvm.base.data.successOr\nimport com.ke.mvvm.base.domian.UseCase\nimport com.ke.mvvm.base.ui.BaseContentViewModel\nimport kotlinx.coroutines.launch\n\nabstract class BattleRateListViewModel<T : BattleRateItem> :\n    BaseContentViewModel<List<BattleRateItem>>(), DefaultLifecycleObserver {\n\n    protected abstract val getBattleRateListUseCase: UseCase<Unit, List<T>>\n\n    override fun onResume(owner: LifecycleOwner) {\n        super.onResume(owner)\n        refresh()\n    }\n\n    internal fun refresh() {\n        viewModelScope.launch {\n            showLoading()\n            showContent(getBattleRateListUseCase(Unit).successOr(emptyList()))\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/DeckBattleRateListViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport javax.inject.Inject\n\n@HiltViewModel\nclass DeckBattleRateListViewModel @Inject constructor(override val getBattleRateListUseCase: GetDeckBattleRateListUseCase) :\n    BattleRateListViewModel<BattleRateItem.DeckBattleRate>()"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/GetDeckBattleRateListUseCase.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport com.ke.hs_tracker.module.db.GameDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass GetDeckBattleRateListUseCase @Inject constructor(\n    private val gameDao: GameDao,\n    @IoDispatcher dispatcher: CoroutineDispatcher\n) : UseCase<Unit, List<BattleRateItem.DeckBattleRate>>(dispatcher) {\n\n    override suspend fun execute(parameters: Unit): List<BattleRateItem.DeckBattleRate> {\n        val games = gameDao.getAll()\n        return games\n            .filter {\n                it.userDeckName.isNotEmpty() && it.userDeckCode.isNotEmpty()\n            }\n            .groupBy {\n                it.userDeckName to it.userDeckCode\n            }.map { map ->\n                BattleRateItem.DeckBattleRate(\n                    map.value.count {\n                        it.isUserWin == true\n                    },\n                    map.value.count {\n                        it.isUserWin == false\n                    },\n                    map.value.count {\n                        it.isUserFirst == false\n                    },\n                    map.key.first,\n                    map.key.second\n                )\n\n            }\n\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/GetHeroBattleRateListUseCase.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport com.ke.hs_tracker.module.db.GameDao\nimport com.ke.hs_tracker.module.di.IoDispatcher\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.mvvm.base.domian.UseCase\nimport kotlinx.coroutines.CoroutineDispatcher\nimport javax.inject.Inject\n\nclass GetHeroBattleRateListUseCase @Inject constructor(\n    @IoDispatcher dispatcher: CoroutineDispatcher,\n    private val gameDao: GameDao\n) : UseCase<Unit, List<BattleRateItem.ClassBattleRate>>(dispatcher) {\n\n    override suspend fun execute(parameters: Unit): List<BattleRateItem.ClassBattleRate> {\n\n        val list = mutableListOf<BattleRateItem.ClassBattleRate>()\n\n        CardClass.values()\n            .filter {\n                it.isHero\n            }\n            .map {\n                it to gameDao.getByHero(it)\n            }.forEach { pair ->\n\n                val heroWinCount = pair.second.count {\n                    it.isUserWin == true\n                }\n                val heroLostCount = pair.second.count {\n                    it.isUserWin == false\n                }\n                val firstHandCount = pair.second.count {\n                    it.isUserFirst == true\n                }\n\n                val item = BattleRateItem.ClassBattleRate(\n                    heroWinCount,\n                    heroLostCount,\n                    firstHandCount,\n                    pair.first\n                )\n                list.add(item)\n            }\n\n        return list\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/HeroBattleRateListViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport javax.inject.Inject\n\n@HiltViewModel\nclass HeroBattleRateListViewModel @Inject constructor(override val getBattleRateListUseCase: GetHeroBattleRateListUseCase) :\n    BattleRateListViewModel<BattleRateItem.ClassBattleRate>()"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/RateByDeckFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport androidx.fragment.app.viewModels\nimport dagger.hilt.android.AndroidEntryPoint\n\n@AndroidEntryPoint\nclass RateByDeckFragment : BattleRateListFragment() {\n\n    override val viewModel: DeckBattleRateListViewModel by viewModels()\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/RateByHeroFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport androidx.fragment.app.viewModels\nimport dagger.hilt.android.AndroidEntryPoint\n\n@AndroidEntryPoint\nclass RateByHeroFragment : BattleRateListFragment() {\n    override val viewModel: HeroBattleRateListViewModel by viewModels()\n\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/summary/SummaryActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.summary\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.provider.Settings\nimport android.view.MenuItem\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport com.google.android.material.tabs.TabLayoutMediator\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.data.PreferenceStorage\nimport com.ke.hs_tracker.module.databinding.ModuleActivitySummaryBinding\nimport com.ke.hs_tracker.module.service.WindowService\nimport com.ke.hs_tracker.module.ui.chart.SummaryChartActivity\nimport com.ke.hs_tracker.module.ui.main.MainActivity\nimport com.ke.hs_tracker.module.ui.records.RecordsActivity\nimport com.ke.hs_tracker.module.ui.settings.SettingsActivity\nimport dagger.hilt.android.AndroidEntryPoint\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass SummaryActivity : AppCompatActivity() {\n\n    @Inject\n    lateinit var preferenceStorage: PreferenceStorage\n\n\n    private lateinit var binding: ModuleActivitySummaryBinding\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivitySummaryBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n//        request()\n\n\n        binding.toolbar.apply {\n            menu.clear()\n            menu.add(\n                0,\n                0,\n                0,\n                R.string.module_settings\n            ).setIcon(R.drawable.module_baseline_settings_white_24dp)\n                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)\n            menu.add(\n                0,\n                2,\n                0,\n                R.string.module_pie_chart\n            ).setIcon(R.drawable.module_baseline_pie_chart_white_24dp)\n                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)\n            menu.add(0, 1, 0, R.string.module_view_all_games)\n\n            setOnMenuItemClickListener {\n\n                when (it.itemId) {\n                    0 -> {\n                        startActivity(Intent(this@SummaryActivity, SettingsActivity::class.java))\n                    }\n                    1 -> {\n                        startActivity(Intent(this@SummaryActivity, RecordsActivity::class.java))\n                    }\n\n                    2 -> {\n                        startActivity(\n                            Intent(\n                                this@SummaryActivity,\n                                SummaryChartActivity::class.java\n                            )\n                        )\n                    }\n                }\n                true\n            }\n        }\n\n        binding.start.setOnClickListener {\n//            startActivity(Intent(this, MainActivity::class.java))\n//            startService(Intent(this, WindowService::class.java))\n\n\n            if (preferenceStorage.floatingEnable) {\n                //\n                if (Settings.canDrawOverlays(applicationContext)) {\n                    startService(Intent(this, WindowService::class.java))\n                } else {\n                    startActivityForResult(\n                        Intent(\n                            Settings.ACTION_MANAGE_OVERLAY_PERMISSION,\n                            Uri.parse(\"package:$packageName\")\n                        ),\n                        101\n                    )\n                }\n            } else {\n                startActivity(Intent(this, MainActivity::class.java))\n            }\n        }\n        val fragments = listOf(RateByHeroFragment(), RateByDeckFragment())\n        val titles = listOf(R.string.module_by_class, R.string.module_by_deck)\n\n        val adapter = object : FragmentStateAdapter(this) {\n            override fun getItemCount() = fragments.size\n\n            override fun createFragment(position: Int) = fragments[position]\n        }\n        binding.viewPager.adapter = adapter\n\n        TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->\n            tab.setText(titles[position])\n        }.attach()\n\n    }\n\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        if (requestCode == 101 && Settings.canDrawOverlays(applicationContext)) {\n            startService(Intent(this, WindowService::class.java))\n        }\n    }\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/support/SupportActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.support\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.Toolbar\nimport com.ke.hs_tracker.module.R\n\nclass SupportActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.module_activity_support)\n\n        findViewById<Toolbar>(R.id.toolbar).setNavigationOnClickListener {\n            onBackPressed()\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/sync/SyncCardDataActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.sync\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatActivity\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivitySyncCardDataBinding\nimport com.ke.hs_tracker.module.ui.summary.SummaryActivity\nimport com.ke.mvvm.base.ui.collectLoadingDialog\nimport com.ke.mvvm.base.ui.collectSnackbarFlow\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass SyncCardDataActivity : AppCompatActivity() {\n    private lateinit var binding: ModuleActivitySyncCardDataBinding\n\n    private val viewModel: SyncCardDataViewModel by viewModels()\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivitySyncCardDataBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        if (viewModel.showBackButton) {\n            binding.toolbar.setNavigationIcon(R.drawable.module_baseline_arrow_back_white_24dp)\n            binding.toolbar.setNavigationOnClickListener {\n                onBackPressed()\n            }\n        }\n\n        collectLoadingDialog(viewModel)\n        collectSnackbarFlow(viewModel)\n\n        binding.sync.setOnClickListener {\n            viewModel.sync(\n                binding.version.text?.toString() ?: \"\",\n                if (binding.chinese.isChecked) \"zhCN\" else \"enUS\"\n            )\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            viewModel.navigationActions.collect {\n                val action: () -> Unit = when (it) {\n                    SyncCardDataNavigationAction.NavigateToBack -> {\n                        {\n                            onBackPressed()\n                        }\n                    }\n                    SyncCardDataNavigationAction.NavigateToMain -> {\n                        {\n                            val intent =\n                                Intent(this@SyncCardDataActivity, SummaryActivity::class.java)\n                            startActivity(intent)\n                            finish()\n                        }\n                    }\n                }\n                AlertDialog.Builder(this@SyncCardDataActivity)\n                    .setTitle(R.string.module_hint)\n                    .setMessage(R.string.module_sync_success)\n                    .setOnDismissListener {\n                        action()\n                    }.setPositiveButton(R.string.module_done, null)\n                    .show()\n\n\n            }\n        }\n\n    }\n\n    companion object {\n        const val EXTRA_SHOW_BACK_BUTTON = \"EXTRA_SHOW_BACK_BUTTON\"\n    }\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/sync/SyncCardDataViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.sync\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.domain.ClearCardTableUseCase\nimport com.ke.hs_tracker.module.domain.GetCardListUseCase\nimport com.ke.hs_tracker.module.domain.InsertCardListToDatabaseUseCase\nimport com.ke.mvvm.base.data.Result\nimport com.ke.mvvm.base.model.SnackbarAction\nimport com.ke.mvvm.base.ui.BaseViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass SyncCardDataViewModel @Inject constructor(\n    private val getCardListUseCase: GetCardListUseCase,\n    private val clearCardTableUseCase: ClearCardTableUseCase,\n    private val insertCardListToDatabaseUseCase: InsertCardListToDatabaseUseCase,\n    savedStateHandle: SavedStateHandle\n) : BaseViewModel() {\n\n    internal val showBackButton =\n        savedStateHandle.get<Boolean>(SyncCardDataActivity.EXTRA_SHOW_BACK_BUTTON) ?: false\n\n    private val _navigationActions =\n        Channel<SyncCardDataNavigationAction>(capacity = Channel.CONFLATED)\n\n    val navigationActions: Flow<SyncCardDataNavigationAction>\n        get() = _navigationActions.receiveAsFlow()\n\n    fun sync(\n        versionCode: String,\n        region: String\n    ) {\n\n        viewModelScope.launch {\n            showLoadingDialog(\"同步中\")\n\n            val code = versionCode.ifEmpty { \"latest\" }\n\n            when (val result = getCardListUseCase(code to region)) {\n                is Result.Success -> {\n                    clearCardTableUseCase(Unit)\n                    insertCardListToDatabaseUseCase(result.data)\n                    dismissLoadingDialog()\n                    _navigationActions.send(if (showBackButton) SyncCardDataNavigationAction.NavigateToBack else SyncCardDataNavigationAction.NavigateToMain)\n                }\n                is Result.Error -> {\n                    result.exception.printStackTrace()\n                    dismissLoadingDialog()\n                    showSnackbar(SnackbarAction(\"从服务器获取数据失败\"))\n//                    throw result.exception\n                }\n            }\n\n        }\n    }\n}\n\nsealed interface SyncCardDataNavigationAction {\n    /**\n     * 去首页\n     */\n    object NavigateToMain : SyncCardDataNavigationAction\n\n    /**\n     * 返回上一个页面\n     */\n    object NavigateToBack : SyncCardDataNavigationAction\n}\n"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/test/CreateRecordActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.test\n\nimport android.content.DialogInterface\nimport androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport androidx.appcompat.app.AlertDialog\nimport androidx.lifecycle.lifecycleScope\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivityCreateRecordBinding\nimport com.ke.hs_tracker.module.db.Game\nimport com.ke.hs_tracker.module.db.GameDao\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.hs_tracker.module.entity.FormatType\nimport com.ke.hs_tracker.module.entity.GameType\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass CreateRecordActivity : AppCompatActivity() {\n\n    @Inject\n    lateinit var gameDao: GameDao\n\n    private val heroClasses = CardClass.values().filter {\n        it.roundIcon != null\n    }\n\n    private var userClass = heroClasses[1]\n\n    private var opponentClass = heroClasses[3]\n\n    lateinit var binding: ModuleActivityCreateRecordBinding\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityCreateRecordBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n\n        refreshUserClass()\n        refreshOpponentClass()\n\n        binding.titleUserClass.setOnClickListener {\n            showClassPickerDialog { dialog, index ->\n                dialog.dismiss()\n                userClass = heroClasses[index]\n                refreshUserClass()\n            }\n        }\n\n        binding.titleOpponentClass.setOnClickListener {\n            showClassPickerDialog { dialog, index ->\n                dialog.dismiss()\n                opponentClass = heroClasses[index]\n                refreshOpponentClass()\n            }\n        }\n\n        binding.create.setOnClickListener {\n            val game = Game(\n                buildNumber = binding.buildNumber.text?.toString() ?: \"\",\n                gameType = getGameType(),\n                formatType = getFormatType(),\n                scenarioID = 2,\n                userName = binding.username.text?.toString() ?: \"\",\n                opponentName = binding.opponentName.text?.toString() ?: \"\",\n                isUserFirst = binding.isUserFirst.isChecked,\n                isUserWin = binding.isUserWon.isChecked,\n                userHero = userClass,\n                opponentHero = opponentClass,\n                userDeckCode = \"AAECAbr5AwSJiwS4oASlrQSEsAQN5boD6LoD77oDm84D8NQDieADiuADpOED0eEDjOQDj+QDr4AEz6wEAA==\",\n                userDeckName = \"天胡\",\n                startTime = System.currentTimeMillis() - 10000,\n                endTime = System.currentTimeMillis()\n            )\n\n            lifecycleScope.launch {\n                gameDao.insert(game)\n                AlertDialog.Builder(this@CreateRecordActivity)\n                    .setTitle(\"提示\")\n                    .setMessage(\"写入成功\")\n                    .setPositiveButton(\"确定\", null)\n                    .show()\n            }\n        }\n    }\n\n    private fun showClassPickerDialog(onSelected: (DialogInterface, Int) -> Unit) {\n        AlertDialog.Builder(this)\n            .setTitle(\"选择职业\")\n            .setSingleChoiceItems(heroClasses.map {\n                getString(it.titleRes)\n            }.toTypedArray(), heroClasses.indexOf(userClass)) { dialog, index ->\n                onSelected(dialog, index)\n            }.show()\n    }\n\n    private fun getGameType(): GameType {\n        return if (binding.rbGameType.checkedRadioButtonId == R.id.game_type_ranked) GameType.Ranked else GameType.Casual\n    }\n\n    private fun getFormatType(): FormatType {\n        return when (binding.rbFormatType.checkedRadioButtonId) {\n            R.id.format_type_standard -> FormatType.Standard\n            R.id.format_type_classic -> FormatType.Classic\n            R.id.format_type_wild -> FormatType.Wild\n            else -> FormatType.Unknown\n        }\n    }\n\n    private fun refreshUserClass() {\n        binding.userClass.setText(userClass.titleRes)\n    }\n\n    private fun refreshOpponentClass() {\n        binding.opponentClass.setText(opponentClass.titleRes)\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/test/LocalFileParserActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.test\n\nimport androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport com.ke.hs_tracker.module.R\n\nclass LocalFileParserActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.module_activity_local_file_parser)\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/test/TestActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.test\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport com.ke.hs_tracker.module.databinding.ModuleActivityTestBinding\nimport com.ke.hs_tracker.module.findHSDataFilesDir\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nclass TestActivity : AppCompatActivity() {\n    private lateinit var binding: ModuleActivityTestBinding\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityTestBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n        binding.createRecord.setOnClickListener {\n            startActivity(Intent(this, CreateRecordActivity::class.java))\n        }\n\n        binding.clearLog.setOnClickListener {\n            lifecycleScope.launch {\n                withContext(Dispatchers.IO) {\n                    val documentFile = findHSDataFilesDir(\"Logs\")?.findFile(\"Power.log\")\n//                        getLogsDir()?.findFile(powerFileName)\n                    documentFile?.apply {\n                        contentResolver.openOutputStream(uri, \"wt\")?.use {\n                            it.write(\"\".encodeToByteArray())\n                            it.flush()\n                            it.close()\n                        }\n                    }\n                }\n\n\n                AlertDialog.Builder(this@TestActivity)\n                    .setTitle(\"提示\")\n                    .setMessage(\"清除成功\")\n                    .setPositiveButton(\"确定\", null)\n                    .show()\n            }\n\n        }\n//        val powerParser: PowerParser = PowerParserImpl()\n\n//        val lineList = mutableListOf<String>()\n//\n//        lineList.clear()\n//        lineList.addAll(\n//            assets.open(\"Power.log\").reader().readLines().toMutableList()\n//        )\n//\n//        val logDocument = findHSDataFilesDir(\"Logs\")\n//\n//\n//        var document = logDocument?.findFile(\"Power.log\")\n//        if (document == null) {\n//            document = logDocument?.createFile(\"plain/text\", \"Power.log\")\n//        }\n//\n//        var writer: OutputStreamWriter? = null\n//        document?.apply {\n//\n//            writer = contentResolver.openOutputStream(uri, \"wa\")\n//                ?.writer()\n//        }\n\n//        binding.clear.setOnClickListener {\n//            document?.apply {\n//                contentResolver.openOutputStream(uri, \"wt\")?.writer()?.write(\"\")\n//\n//            }\n//\n//            assets.open(\"Power.log\").reader().apply {\n//                lineList.clear()\n//\n//                lineList.addAll(readLines().toMutableList())\n//                close()\n//            }\n//\n//\n//        }\n\n\n//        powerParser.powerTagListener = {\n//\n//\n//        }\n//        var counter = 0\n//        binding.next.setOnClickListener {\n////                val target = mutableListOf<String>()\n//            repeat(1024) {\n//                counter++\n//                val line = lineList.removeFirstOrNull()\n//                if (line != null) {\n////                        target.add(line)\n//                    writer?.appendLine(line)\n////                    powerParser.parse(line)\n//                }\n//            }\n//            writer?.flush()\n//\n//\n//        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/theme/ThemeActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.theme\n\nimport android.os.Bundle\nimport android.widget.CompoundButton\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.app.AppCompatDelegate\nimport com.ke.hs_tracker.module.data.PreferenceStorage\nimport com.ke.hs_tracker.module.databinding.ModuleActivityThemeBinding\nimport dagger.hilt.android.AndroidEntryPoint\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass ThemeActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener {\n\n    @Inject\n    lateinit var preferenceStorage: PreferenceStorage\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val binding = ModuleActivityThemeBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n        binding.light.tag = AppCompatDelegate.MODE_NIGHT_NO\n        binding.dark.tag = AppCompatDelegate.MODE_NIGHT_YES\n        binding.system.tag = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM\n\n        val buttonList = listOf(\n            binding.light,\n            binding.dark,\n            binding.system\n        )\n\n        buttonList.forEach {\n            it.isChecked = it.tag == preferenceStorage.theme\n        }\n\n        buttonList.forEach {\n            it.setOnCheckedChangeListener(this)\n        }\n\n\n    }\n\n    override fun onCheckedChanged(button: CompoundButton, checked: Boolean) {\n\n        if (checked) {\n            val theme = button.tag as? Int ?: return\n            preferenceStorage.theme = theme\n        }\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/writeconfig/WriteConfigActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.writeconfig\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivityWriteConfigBinding\nimport com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity\nimport com.ke.hs_tracker.module.writeLogConfigFile\nimport kotlinx.coroutines.launch\n\nclass WriteConfigActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n//        setContentView(R.layout.module_activity_write_config)\n        val binding = ModuleActivityWriteConfigBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        val rewrite = intent.getBooleanExtra(EXTRA_REWRITE, false)\n\n        if (rewrite) {\n            binding.forceWrite.isEnabled = false\n            binding.forceWrite.isChecked = true\n            binding.toolbar.apply {\n                setNavigationIcon(R.drawable.module_baseline_arrow_back_white_24dp)\n                setNavigationOnClickListener {\n                    onBackPressed()\n                }\n            }\n        }\n\n        binding.content.text = assets.open(\"log.config\").reader().readText()\n\n        binding.writeIn.setOnClickListener {\n//            findHSDataFilesDir(\"log.config\")\n            lifecycleScope.launch {\n                if (writeLogConfigFile(\n                        binding.forceWrite.isChecked\n                    )\n                ) {\n                    if (rewrite) {\n                        onBackPressed()\n                    } else {\n                        startActivity(\n                            Intent(\n                                this@WriteConfigActivity,\n                                SyncCardDataActivity::class.java\n                            )\n                        )\n                        finish()\n                    }\n                } else {\n                    AlertDialog.Builder(this@WriteConfigActivity)\n                        .setTitle(R.string.module_hint)\n                        .setMessage(R.string.module_write_config_failed)\n                        .setPositiveButton(R.string.module_done, null)\n                        .show()\n                }\n\n            }\n        }\n\n    }\n\n    companion object {\n        const val EXTRA_REWRITE = \"EXTRA_REWRITE\"\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/zonecards/ZoneCardsActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.zonecards\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport com.bumptech.glide.Glide\nimport com.ke.hs_tracker.module.databinding.ModuleActivityZoneCardsBinding\nimport com.ke.hs_tracker.module.databinding.ModuleItemZoneCardBinding\nimport com.ke.hs_tracker.module.entity.ZoneCard\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\n\nclass ZoneCardsActivity : AppCompatActivity() {\n\n    private val adapter = object : BaseViewBindingAdapter<ZoneCard, ModuleItemZoneCardBinding>() {\n        override fun bindItem(\n            item: ZoneCard,\n            viewBinding: ModuleItemZoneCardBinding,\n            viewType: Int,\n            position: Int\n        ) {\n            viewBinding.apply {\n                cost.text = item.card?.cost?.toString() ?: \"\"\n                name.text = item.card?.name ?: \"未知卡牌\"\n                this.position.text = item.position.toString()\n                item.card?.id?.let {\n                    Glide.with(this@ZoneCardsActivity)\n                        .load(\"https://art.hearthstonejson.com/v1/tiles/${it}.png\")\n                        .into(imageTile)\n                }\n            }\n        }\n\n        override fun createViewBinding(\n            inflater: LayoutInflater,\n            parent: ViewGroup,\n            viewType: Int\n        ): ModuleItemZoneCardBinding {\n            return ModuleItemZoneCardBinding.inflate(inflater, parent, false)\n        }\n\n    }\n\n    private lateinit var binding: ModuleActivityZoneCardsBinding\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityZoneCardsBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        binding.recyclerView.adapter = adapter\n        binding.recyclerView.addItemDecoration(\n            DividerItemDecoration(\n                this,\n                DividerItemDecoration.VERTICAL\n            )\n        )\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n        val zoneCardList = intent.getParcelableArrayListExtra<ZoneCard>(EXTRA_KEY_ZONE_CARD_LIST)\n\n        adapter.setList(zoneCardList?.sortedBy {\n            it.position\n        })\n\n\n    }\n\n    companion object {\n        const val EXTRA_KEY_ZONE_CARD_LIST = \"EXTRA_KEY_ZONE_CARD_LIST\"\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ListModeFragment.kt",
    "content": "package com.ke.hs_tracker.module.ui.zoneevents\n\nimport android.os.Bundle\nimport androidx.fragment.app.Fragment\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.fragment.app.activityViewModels\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport com.hi.dhl.binding.viewbind\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleFragmentListModeBinding\nimport com.ke.hs_tracker.module.databinding.ModuleItemZoneEventListModeBinding\nimport com.ke.hs_tracker.module.entity.Zone\nimport com.ke.hs_tracker.module.entity.ZoneCard\nimport com.ke.mvvm.base.ui.BaseViewBindingAdapter\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\ninternal class ListModeFragment : Fragment() {\n\n    private val onChipClickListener = View.OnClickListener {\n        (it.tag as? List<ZoneCard>)?.apply {\n            if (isNotEmpty()) {\n                (activity as? ZoneEventsActivity)?.toZoneCardsActivity(this)\n            }\n        }\n    }\n\n    private val adapter by lazy {\n        object : BaseViewBindingAdapter<GameCardCollections, ModuleItemZoneEventListModeBinding>() {\n            override fun bindItem(\n                item: GameCardCollections,\n                viewBinding: ModuleItemZoneEventListModeBinding,\n                viewType: Int,\n                position: Int\n            ) {\n                viewBinding.apply {\n                    userDeck.text = \"玩家牌库 ${item.userDeckCardList.size}\"\n                    userHand.text = \"玩家手牌 ${item.userHandCardList.size}\"\n                    userPlay.text = \"玩家战场 ${item.userPlayCardList.size}\"\n                    userGraveyard.text = \"玩家墓地 ${item.userGraveyardCardList.size}\"\n                    userSecret.text = \"玩家奥秘 ${item.userSecretCardList.size}\"\n                    opponentDeck.text = \"对手牌库 ${item.opponentDeckCardList.size}\"\n                    opponentHand.text = \"对手手牌 ${item.opponentHandCardList.size}\"\n                    opponentPlay.text = \"对手战场 ${item.opponentPlayCardList.size}\"\n                    opponentGraveyard.text = \"对手墓地 ${item.opponentGraveyardCardList.size}\"\n                    opponentSecret.text = \"对手奥秘 ${item.opponentSecretCardList.size}\"\n\n                    userDeck.setOnClickListener(onChipClickListener)\n                    userHand.setOnClickListener(onChipClickListener)\n                    userPlay.setOnClickListener(onChipClickListener)\n                    userGraveyard.setOnClickListener(onChipClickListener)\n                    userSecret.setOnClickListener(onChipClickListener)\n                    opponentDeck.setOnClickListener(onChipClickListener)\n                    opponentHand.setOnClickListener(onChipClickListener)\n                    opponentPlay.setOnClickListener(onChipClickListener)\n                    opponentGraveyard.setOnClickListener(onChipClickListener)\n                    opponentSecret.setOnClickListener(onChipClickListener)\n\n                    userDeck.tag = item.userDeckCardList\n                    userHand.tag = item.userHandCardList\n                    userPlay.tag = item.userPlayCardList\n                    userGraveyard.tag = item.userGraveyardCardList\n                    userSecret.tag = item.userSecretCardList\n                    opponentDeck.tag = item.opponentDeckCardList\n                    opponentHand.tag = item.opponentHandCardList\n                    opponentPlay.tag = item.opponentPlayCardList\n                    opponentGraveyard.tag = item.opponentGraveyardCardList\n                    opponentSecret.tag = item.opponentSecretCardList\n\n//                    userDeck.setOnClickListener {\n//                        if (item.userDeckCardList.isNotEmpty()) {\n//                            (activity as? ZoneEventsActivity)?.toZoneCardsActivity(item.userDeckCardList)\n//                        }\n//                    }\n                }\n            }\n\n            override fun createViewBinding(\n                inflater: LayoutInflater,\n                parent: ViewGroup,\n                viewType: Int\n            ): ModuleItemZoneEventListModeBinding {\n                return ModuleItemZoneEventListModeBinding.inflate(inflater, parent, false)\n            }\n\n        }\n    }\n\n    private val binding: ModuleFragmentListModeBinding by viewbind()\n\n    private val zoneEventsViewModel: ZoneEventsViewModel by activityViewModels()\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        binding.recyclerView.adapter = adapter\n        binding.recyclerView.addItemDecoration(\n            DividerItemDecoration(\n                requireContext(),\n                DividerItemDecoration.VERTICAL\n            )\n        )\n\n        launchAndRepeatWithViewLifecycle {\n            zoneEventsViewModel.collectionsList.collect {\n                adapter.setList(it)\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ZoneEventsActivity.kt",
    "content": "package com.ke.hs_tracker.module.ui.zoneevents\n\nimport android.content.Intent\nimport androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport android.os.Parcelable\nimport androidx.activity.viewModels\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport com.ke.hs_tracker.module.R\nimport com.ke.hs_tracker.module.databinding.ModuleActivityZoneEventsBinding\nimport com.ke.hs_tracker.module.entity.ZoneCard\nimport com.ke.hs_tracker.module.ui.common.LoadingFragment\nimport com.ke.hs_tracker.module.ui.zonecards.ZoneCardsActivity\nimport com.ke.mvvm.base.ui.FragmentViewPager2Adapter\nimport com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.flow.collect\n\n@AndroidEntryPoint\nclass ZoneEventsActivity : AppCompatActivity() {\n    private val zoneEventsViewModel: ZoneEventsViewModel by viewModels()\n\n    private lateinit var binding: ModuleActivityZoneEventsBinding\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ModuleActivityZoneEventsBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        binding.toolbar.setNavigationOnClickListener {\n            onBackPressed()\n        }\n\n\n        val fragmentList = listOf(LoadingFragment(), ListModeFragment(), ListModeFragment())\n\n        //禁止左右滑动\n        binding.viewPager.isUserInputEnabled = false\n\n        binding.viewPager.adapter = object : FragmentStateAdapter(this) {\n            override fun getItemCount(): Int {\n                return fragmentList.size\n            }\n\n            override fun createFragment(position: Int): Fragment {\n                return fragmentList[position]\n            }\n        }\n\n        launchAndRepeatWithViewLifecycle {\n            zoneEventsViewModel.currentFragmentIndex.collect {\n                binding.toggle.isVisible = it != 0\n                binding.viewPager.currentItem = it\n            }\n        }\n\n    }\n\n    internal fun toZoneCardsActivity(list: List<ZoneCard>) {\n        val intent = Intent(this, ZoneCardsActivity::class.java)\n        intent.putParcelableArrayListExtra(\n            ZoneCardsActivity.EXTRA_KEY_ZONE_CARD_LIST,\n            arrayListOf<Parcelable?>().apply {\n                addAll(list)\n            })\n        startActivity(intent)\n    }\n\n    companion object {\n        const val EXTRA_KEY_ID = \"EXTRA_KEY_ID\"\n    }\n}"
  },
  {
    "path": "module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ZoneEventsViewModel.kt",
    "content": "package com.ke.hs_tracker.module.ui.zoneevents\n\nimport android.os.Parcel\nimport android.os.Parcelable\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.ke.hs_tracker.module.db.CardDao\nimport com.ke.hs_tracker.module.db.ZonePositionChangedEvent\nimport com.ke.hs_tracker.module.db.ZonePositionChangedEventDao\nimport com.ke.hs_tracker.module.entity.Card\nimport com.ke.hs_tracker.module.entity.Zone\nimport com.ke.hs_tracker.module.entity.ZoneCard\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.parcelize.Parcelize\nimport javax.inject.Inject\n\n@HiltViewModel\nclass ZoneEventsViewModel @Inject constructor(\n    private val zonePositionChangedEventDao: ZonePositionChangedEventDao,\n    private val cardDao: CardDao,\n    savedStateHandle: SavedStateHandle\n) :\n    ViewModel() {\n\n    private val _currentFragmentIndex = MutableStateFlow(0)\n\n    internal val currentFragmentIndex: StateFlow<Int>\n        get() = _currentFragmentIndex\n\n    private val gameId = savedStateHandle.get<String>(ZoneEventsActivity.EXTRA_KEY_ID)!!\n\n    private val gameCardCollectionsList = mutableListOf<GameCardCollections>().apply {\n        add(GameCardCollections())\n    }\n\n    private val _collectionsList = MutableStateFlow<List<GameCardCollections>>(emptyList())\n\n    internal val collectionsList: StateFlow<List<GameCardCollections>>\n        get() = _collectionsList\n\n    init {\n        viewModelScope.launch {\n            withContext(Dispatchers.IO) {\n\n                val allCard = cardDao.getAll()\n                val list = zonePositionChangedEventDao.getAllByGameId(gameId)\n\n                if (list.isEmpty()) {\n                    return@withContext\n                }\n                val stack = mutableListOf<ZonePositionChangedEvent>()\n\n                list.forEach {\n\n                    if (it.entityId == stack.lastOrNull()?.entityId) {\n                        stack.add(it)\n                    } else {\n                        flushStack(stack, gameCardCollectionsList, allCard)\n                    }\n\n                    if (stack.isEmpty()) {\n                        stack.add(it)\n                    }\n                }\n\n                flushStack(stack, gameCardCollectionsList, allCard)\n\n                _currentFragmentIndex.value = 1\n\n                _collectionsList.value = gameCardCollectionsList\n\n            }\n\n        }\n    }\n\n\n    companion object {\n        private fun flushStack(\n            list: MutableList<ZonePositionChangedEvent>,\n            mutableList: MutableList<GameCardCollections>,\n            allCardList: List<Card>\n        ) {\n\n            val last = mutableList.last()\n            if (list.isEmpty()) {\n                return\n            }\n            when (list.size) {\n                1 -> {\n                    //仅改变区域\n                    val event = list.first()\n\n                    mutableList.add(last.update(event, allCardList))\n\n                }\n                2 -> {\n                    //改变区域和位置\n                    val first = list[0]\n                    val second = list[1]\n\n\n                    mutableList.add(last.update(first.plus(second), allCardList))\n                }\n                3 -> {\n                    val first = list[0]\n                    val second = list[1]\n                    val third = list[2]\n                    mutableList.add(last.update(first.plusPlus(second, third), allCardList))\n\n                }\n                else -> {\n                    list.forEach {\n                        flushStack(\n                            mutableListOf(it), mutableList, allCardList\n                        )\n                    }\n                }\n            }\n            list.clear()\n        }\n    }\n}\n\n@Parcelize\ninternal data class GameCardCollections(\n    private val _userDeckCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _opponentDeckCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _userHandCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _opponentHandCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _userPlayCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _opponentPlayCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _userGraveyardCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _opponentGraveyardCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _userSecretCardList: MutableList<ZoneCard> = mutableListOf(),\n    private val _opponentSecretCardList: MutableList<ZoneCard> = mutableListOf(),\n\n    ) : Parcelable {\n\n    /**\n     * 深拷贝\n     */\n    private fun deepClone(): GameCardCollections {\n        var parcel: Parcel? = null\n        try {\n            parcel = Parcel.obtain()\n            parcel.writeParcelable(this, 0)\n\n            parcel.setDataPosition(0)\n            return parcel.readParcelable(this.javaClass.classLoader)!!\n        } finally {\n\n            parcel?.recycle()\n        }\n    }\n\n    val userDeckCardList: List<ZoneCard>\n        get() = _userDeckCardList\n\n    val opponentDeckCardList: List<ZoneCard>\n        get() = _opponentDeckCardList\n\n    val userHandCardList: List<ZoneCard>\n        get() = _userHandCardList\n\n    val opponentHandCardList: List<ZoneCard>\n        get() = _opponentHandCardList\n\n    val userPlayCardList: List<ZoneCard>\n        get() = _userPlayCardList\n\n    val opponentPlayCardList: List<ZoneCard>\n        get() = _opponentPlayCardList\n\n    val userGraveyardCardList: List<ZoneCard>\n        get() = _userGraveyardCardList\n\n    val opponentGraveyardCardList: List<ZoneCard>\n        get() = _opponentGraveyardCardList\n\n    val userSecretCardList: List<ZoneCard>\n        get() = _userSecretCardList\n\n    val opponentSecretCardList: List<ZoneCard>\n        get() = _opponentSecretCardList\n\n    fun update(event: ZonePositionChangedEvent, allCardList: List<Card>): GameCardCollections {\n\n        val copy = deepClone()\n\n        val oldList = when (event.currentZone) {\n            Zone.Play -> if (event.isUser) copy._userPlayCardList else copy._opponentPlayCardList\n            Zone.Deck -> if (event.isUser) copy._userDeckCardList else copy._opponentDeckCardList\n\n            Zone.Graveyard -> if (event.isUser) copy._userGraveyardCardList else copy._opponentGraveyardCardList\n            Zone.Hand -> if (event.isUser) copy._userHandCardList else copy._opponentHandCardList\n\n            Zone.Secret -> if (event.isUser) copy._userSecretCardList else copy._opponentSecretCardList\n            Zone.SetAside, Zone.RemovedFromGame -> null\n\n            else -> throw RuntimeException(\"oldList 不支持的区域类型 ${event.currentZone}\")\n        }\n\n        val newList = when (event.newZone) {\n            Zone.Play -> if (event.isUser) copy._userPlayCardList else copy._opponentPlayCardList\n            Zone.Deck -> if (event.isUser) copy._userDeckCardList else copy._opponentDeckCardList\n\n            Zone.Graveyard -> if (event.isUser) copy._userGraveyardCardList else copy._opponentGraveyardCardList\n            Zone.Hand -> if (event.isUser) copy._userHandCardList else copy._opponentHandCardList\n\n            Zone.Secret -> if (event.isUser) copy._userSecretCardList else copy._opponentSecretCardList\n            //移除掉\n            Zone.SetAside, Zone.RemovedFromGame -> null\n\n            else -> {\n\n                throw RuntimeException(\"newList 不支持的区域类型 ${event.newZone}\")\n            }\n        }\n\n        if (event.currentZone == event.newZone) {\n            addCardToList(newList, allCardList, event)\n        } else {\n            oldList?.removeAll {\n                it.entityId == event.entityId\n            }\n\n            addCardToList(newList, allCardList, event)\n        }\n\n\n\n\n        return copy\n\n    }\n\n    private fun addCardToList(\n        newList: MutableList<ZoneCard>?,\n        allCardList: List<Card>,\n        event: ZonePositionChangedEvent\n    ) {\n        val target = newList ?: emptyList()\n\n        if (target.find {\n                it.entityId == event.entityId\n            } != null) {\n            //存在的话就不能插入\n            return\n        }\n\n        newList?.add(\n            ZoneCard(\n                allCardList.find { it.id == event.cardId },\n                event.entityId,\n                event.newPosition\n            )\n        )\n    }\n}\n\n"
  },
  {
    "path": "module/src/main/res/color/module_game_state.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item android:color=\"@color/module_loss\" android:state_enabled=\"false\" />\n    <item android:color=\"@color/module_win\" android:state_enabled=\"true\" />\n</selector>"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_arrow_back_white_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:autoMirrored=\"true\" android:height=\"24dp\" android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n    \n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_clear_black_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#000000\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z\" />\n\n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_clear_red_500_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#F44336\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z\" />\n\n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_done_black_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z\"/>\n    \n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_done_green_500_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#4CAF50\" android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z\"/>\n    \n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_done_white_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z\"/>\n    \n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_drag_handle_white_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M20,9H4v2h16V9zM4,15h16v-2H4V15z\" />\n\n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_keyboard_arrow_right_grey_500_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:autoMirrored=\"true\" android:height=\"24dp\" android:tint=\"#9E9E9E\" android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z\"/>\n    \n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_pie_chart_white_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M11,2v20c-5.07,-0.5 -9,-4.79 -9,-10s3.93,-9.5 9,-10zM13.03,2v8.99L22,10.99c-0.47,-4.74 -4.24,-8.52 -8.97,-8.99zM13.03,13.01L13.03,22c4.74,-0.47 8.5,-4.25 8.97,-8.99h-8.97z\" />\n\n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_play_arrow_white_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M8,5v14l11,-7z\" />\n\n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_settings_white_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z\"/>\n    \n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_sync_white_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z\"/>\n    \n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable/module_baseline_zoom_out_map_white_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M15,3l2.3,2.3l-2.89,2.87l1.42,1.42L18.7,6.7L21,9V3H15zM3,9l2.3,-2.3l2.87,2.89l1.42,-1.42L6.7,5.3L9,3H3V9zM9,21l-2.3,-2.3l2.89,-2.87l-1.42,-1.42L5.3,17.3L3,15v6H9zM21,15l-2.3,2.3l-2.87,-2.89l-1.42,1.42l2.89,2.87L15,21h6V15z\" />\n\n</vector>\n"
  },
  {
    "path": "module/src/main/res/drawable-xxxhdpi/module_bg_splash.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <solid android:color=\"@color/module_white\" />\n        </shape>\n    </item>\n\n    <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@drawable/module_image_splash\" />\n    </item>\n</layer-list>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_class_battle_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.classbattledetail.ClassBattleDetailActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_class_battle_detail\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:paddingVertical=\"16dp\"\n            android:text=\"@string/module_opponent_class\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:paddingVertical=\"16dp\"\n            android:text=\"@string/module_times\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:paddingVertical=\"16dp\"\n            android:text=\"@string/module_win\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:paddingVertical=\"16dp\"\n            android:text=\"@string/module_loss\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:paddingVertical=\"16dp\"\n            android:text=\"@string/module_win_rate\" />\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_grey500\" />\n\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n        tools:listitem=\"@layout/module_item_class_battle_detail\" />\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_create_record.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.test.CreateRecordActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"生成对战记录\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\">\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/username\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:hint=\"玩家名称\"\n                    android:text=\"阿克蒙德#51240\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/opponent_name\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:hint=\"对手名称\"\n                    android:text=\"饭岛爱#51240\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/build_number\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:hint=\"序列号\"\n                    android:inputType=\"number\"\n                    android:text=\"42112\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n\n            <com.google.android.material.switchmaterial.SwitchMaterial\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"玩家先手\"\n                android:padding=\"16dp\"\n                android:id=\"@+id/is_user_first\"/>\n            <com.google.android.material.switchmaterial.SwitchMaterial\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"玩家胜利\"\n                android:padding=\"16dp\"\n                android:id=\"@+id/is_user_won\"/>\n\n            <RadioGroup\n                android:id=\"@+id/rb_game_type\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n\n                <TextView\n                    style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"16dp\"\n                    android:text=\"游戏类型\" />\n\n                <com.google.android.material.radiobutton.MaterialRadioButton\n                    android:id=\"@+id/game_type_ranked\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:checked=\"true\"\n                    android:text=\"排名\" />\n\n                <com.google.android.material.radiobutton.MaterialRadioButton\n                    android:id=\"@+id/game_type_casual\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"休闲\" />\n            </RadioGroup>\n\n            <RadioGroup\n                android:id=\"@+id/rb_format_type\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n\n                <TextView\n                    style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"16dp\"\n                    android:text=\"卡牌类型\" />\n\n                <com.google.android.material.radiobutton.MaterialRadioButton\n                    android:id=\"@+id/format_type_standard\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:checked=\"true\"\n                    android:text=\"@string/module_standard\" />\n\n                <com.google.android.material.radiobutton.MaterialRadioButton\n                    android:id=\"@+id/format_type_wild\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/module_wild\" />\n\n                <com.google.android.material.radiobutton.MaterialRadioButton\n                    android:id=\"@+id/format_type_classic\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/module_classic\" />\n            </RadioGroup>\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n                <TextView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"16dp\"\n                    style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                    android:text=\"玩家职业\"\n                    android:id=\"@+id/title_user_class\"\n                    android:background=\"?selectableItemBackground\"/>\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/module_druid\"\n                    android:layout_gravity=\"end|center_vertical\"\n                    android:layout_marginEnd=\"16dp\"\n                    android:id=\"@+id/user_class\"/>\n            </FrameLayout>\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\">\n                <TextView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"16dp\"\n                    style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                    android:text=\"对手职业\"\n                    android:id=\"@+id/title_opponent_class\"\n                    android:background=\"?selectableItemBackground\"/>\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/module_druid\"\n                    android:layout_gravity=\"end|center_vertical\"\n                    android:layout_marginEnd=\"16dp\"\n                    android:id=\"@+id/opponent_class\"/>\n            </FrameLayout>\n\n            <Button\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"生成\"\n                android:layout_margin=\"24dp\"\n                android:id=\"@+id/create\"/>\n        </LinearLayout>\n    </androidx.core.widget.NestedScrollView>\n\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_deck_battle_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.deckbattledetail.DeckBattleDetailActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_deck_battle_detail\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <com.google.android.material.tabs.TabLayout\n        android:id=\"@+id/tab_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <androidx.viewpager2.widget.ViewPager2\n        android:id=\"@+id/view_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\" />\n\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_deck_code_parser.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"#333\"\n    tools:context=\".ui.deck.DeckCodeParserActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_code_parser\"\n            app:titleTextColor=\"@color/module_white\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:id=\"@+id/til_code\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"8dp\"\n        android:hint=\"@string/module_code\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\"\n        >\n\n        <com.google.android.material.textfield.TextInputEditText\n            android:id=\"@+id/code\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_default_deck_code\" />\n\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <Button\n        android:id=\"@+id/start\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"36dp\"\n        android:layout_marginTop=\"24dp\"\n        android:layout_marginEnd=\"36dp\"\n        android:text=\"@string/module_start\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/til_code\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_marginTop=\"16dp\"\n        android:layout_marginBottom=\"16dp\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/start\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_diagnose.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.diagnose.DiagnoseActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_diagnose\"\n            app:titleTextColor=\"@color/module_white\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <Button\n        android:id=\"@+id/check_config_file\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"8dp\"\n        android:text=\"@string/module_check_config_file\" />\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_filter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/module_filter_activity_color\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.filter.FilterActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_filter\"\n            app:titleTextColor=\"@color/module_white\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <RadioGroup\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:id=\"@+id/radio_group\"\n            android:orientation=\"vertical\">\n\n\n            <RadioButton\n                android:id=\"@+id/rb_class\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/module_class\" />\n\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_class\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"8dp\" />\n\n            <RadioButton\n                android:id=\"@+id/rb_rarity\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/module_rarity\"\n                />\n\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_rarity\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"8dp\" />\n\n            <RadioButton\n                android:id=\"@+id/rb_minion\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/module_minion\"\n                />\n\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_race\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"8dp\" />\n\n            <RadioButton\n                android:id=\"@+id/rb_spell\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/module_spell\"\n                />\n\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_spell\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"8dp\" />\n\n            <RadioButton\n                android:id=\"@+id/rb_weapon\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/module_weapon\"\n                />\n\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_weapon\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"8dp\" />\n\n            <RadioButton\n                android:id=\"@+id/rb_hero\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/module_hero\"\n                />\n\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_hero\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"8dp\" />\n\n            <RadioButton\n                android:id=\"@+id/rb_mechanics\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/module_mechanics\"\n                />\n\n\n            <com.google.android.material.chip.ChipGroup\n                android:id=\"@+id/chip_group_mechanics\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"8dp\" />\n\n        </RadioGroup>\n    </androidx.core.widget.NestedScrollView>\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_local_file_parser.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.test.LocalFileParserActivity\">\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.main.MainActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:elevation=\"5dp\"\n        android:background=\"#00000000\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:title=\"@string/module_app_name\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:titleTextColor=\"@color/module_white\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n    <com.google.android.material.tabs.TabLayout\n        android:id=\"@+id/tab_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\"\n        android:background=\"#00000000\"\n        app:tabMode=\"fixed\">\n\n\n    </com.google.android.material.tabs.TabLayout>\n\n\n    <androidx.viewpager2.widget.ViewPager2\n        android:id=\"@+id/view_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/tab_layout\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_migrate_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.migrate.MigrateMainActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_data_transfer\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <TextView\n        android:id=\"@+id/to_another_phone\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?android:selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_title_migrate_date_to_another_phone\" />\n\n\n    <TextView\n        android:id=\"@+id/to_this_phone\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?android:selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_title_migrate_data_to_this_phone\" />\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_permissions.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center_horizontal\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.permissions.PermissionsActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:title=\"@string/module_set_permissions\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <!--    <TextView-->\n    <!--        android:id=\"@+id/step1\"-->\n    <!--        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"-->\n    <!--        android:layout_width=\"match_parent\"-->\n    <!--        android:layout_height=\"wrap_content\"-->\n    <!--        android:padding=\"16dp\"-->\n    <!--        android:text=\"@string/module_permissions_step_1\"-->\n    <!--        app:drawableEndCompat=\"@drawable/module_baseline_done_green_500_24dp\" />-->\n\n    <!--    <View-->\n    <!--        android:layout_width=\"match_parent\"-->\n    <!--        android:layout_height=\"1dp\"-->\n    <!--        android:background=\"#ccc\" />-->\n\n    <TextView\n        android:id=\"@+id/step2\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_permissions_step_2\"\n        app:drawableEndCompat=\"@drawable/module_baseline_done_green_500_24dp\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"#ccc\" />\n\n    <!--    <TextView-->\n    <!--        android:id=\"@+id/step3\"-->\n    <!--        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"-->\n    <!--        android:layout_width=\"match_parent\"-->\n    <!--        android:layout_height=\"wrap_content\"-->\n    <!--        android:padding=\"16dp\"-->\n    <!--        android:text=\"@string/module_permissions_step_3\"-->\n    <!--        app:drawableEndCompat=\"@drawable/module_baseline_done_green_500_24dp\" />-->\n\n\n    <Button\n        android:id=\"@+id/next\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"8dp\"\n        android:text=\"@string/module_next\" />\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_records.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.records.RecordsActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:title=\"@string/module_records\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n        android:id=\"@+id/swipe_refresh_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/recycler_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n            tools:listitem=\"@layout/module_item_record\" />\n    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n\n\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.settings.SettingsActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:elevation=\"5dp\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_settings\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n    <TextView\n        android:id=\"@+id/theme\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_theme\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_divider\" />\n\n\n    <com.google.android.material.switchmaterial.SwitchMaterial\n        android:id=\"@+id/floating\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_floating_track\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\" />\n\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_divider\" />\n\n\n    <TextView\n        android:id=\"@+id/language\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"1dp\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_language\"\n        android:visibility=\"gone\" />\n\n\n    <TextView\n        android:id=\"@+id/sync\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"1dp\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_sync_card_data\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_divider\"\n        app:layout_constraintTop_toBottomOf=\"@id/sync\" />\n\n    <TextView\n        android:id=\"@+id/migrate\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"1dp\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_data_transfer\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_divider\"\n        app:layout_constraintTop_toBottomOf=\"@id/sync\" />\n\n    <TextView\n        android:id=\"@+id/code_parser\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"1dp\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_code_parser\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_divider\"\n        app:layout_constraintTop_toBottomOf=\"@id/sync\" />\n\n    <TextView\n        android:id=\"@+id/rewrite_config_file\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"1dp\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_rewrite_config_file\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_divider\"\n        app:layout_constraintTop_toBottomOf=\"@id/sync\" />\n\n    <com.google.android.material.switchmaterial.SwitchMaterial\n        android:id=\"@+id/save_log_file\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:checked=\"true\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_save_log_file\"\n        android:textSize=\"16sp\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_divider\" />\n\n    <LinearLayout\n        android:id=\"@+id/ll_contact_author\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"1dp\"\n        android:background=\"?selectableItemBackground\"\n        android:orientation=\"vertical\"\n        android:padding=\"16dp\">\n\n\n        <TextView\n            android:id=\"@+id/contact_author\"\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_contact_author\"\n            app:layout_constraintTop_toBottomOf=\"@id/sync\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Caption\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_author_email\" />\n\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@color/module_divider\" />\n\n    <TextView\n        android:id=\"@+id/support\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"1dp\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_support\" />\n\n    <LinearLayout\n        android:visibility=\"gone\"\n        android:id=\"@+id/ll_source\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:orientation=\"vertical\"\n        android:padding=\"16dp\">\n\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n\n            android:text=\"@string/module_code\"\n            app:layout_constraintTop_toBottomOf=\"@id/sync\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Caption\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_github_address\" />\n\n    </LinearLayout>\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_socket_client.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_receive\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:hint=\"IP\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            android:id=\"@+id/ip_address\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:hint=\"@string/module_port\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            android:id=\"@+id/ip_port\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:inputType=\"number\"\n            android:text=\"10000\" />\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <Button\n        android:id=\"@+id/start\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:text=\"@string/module_start\" />\n\n    <ProgressBar\n        android:id=\"@+id/loading_progress\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_margin=\"24dp\"\n        android:visibility=\"gone\" />¬\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_socket_server.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.migrate.SocketServerActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_receive\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:hint=\"IP\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            android:id=\"@+id/ip_address\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:enabled=\"false\" />\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:hint=\"@string/module_port\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            android:id=\"@+id/ip_port\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:inputType=\"number\"\n            android:text=\"10000\" />\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <Button\n        android:id=\"@+id/start\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:text=\"@string/module_start\" />\n\n    <ProgressBar\n        android:id=\"@+id/loading_progress\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_margin=\"24dp\"\n        android:visibility=\"gone\" />¬\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_summary.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.summary.SummaryActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:elevation=\"5dp\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:title=\"@string/module_summary\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n\n    <com.google.android.material.tabs.TabLayout\n        android:id=\"@+id/tab_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\" />\n\n    <androidx.viewpager2.widget.ViewPager2\n        android:id=\"@+id/view_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/tab_layout\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/start\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:contentDescription=\"@null\"\n        android:src=\"@drawable/module_baseline_play_arrow_white_24dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:tint=\"@null\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_summary_chart.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.chart.SummaryChartActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_summary\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"16dp\"\n                android:text=\"@string/module_all_win_rate\" />\n\n            <com.github.mikephil.charting.charts.PieChart\n                android:id=\"@+id/all\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"@dimen/module_chart_height\" />\n\n            <TextView\n                style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"16dp\"\n                android:text=\"@string/module_first_hand_percent\" />\n\n            <com.github.mikephil.charting.charts.PieChart\n                android:id=\"@+id/first_hand_percent\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"@dimen/module_chart_height\" />\n\n            <TextView\n                style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"16dp\"\n                android:text=\"@string/module_first_hand_rate\" />\n\n            <com.github.mikephil.charting.charts.PieChart\n                android:id=\"@+id/first_hand_rate\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"@dimen/module_chart_height\" />\n\n            <TextView\n                style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"16dp\"\n                android:text=\"@string/module_second_hand_rate\" />\n\n            <com.github.mikephil.charting.charts.PieChart\n                android:id=\"@+id/second_hand_rate\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"@dimen/module_chart_height\" />\n\n            <TextView\n                style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:padding=\"16dp\"\n                android:text=\"@string/module_class_distribution\" />\n\n            <com.github.mikephil.charting.charts.PieChart\n                android:id=\"@+id/class_distribution\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"@dimen/module_chart_height\" />\n        </LinearLayout>\n\n\n    </androidx.core.widget.NestedScrollView>\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_support.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.support.SupportActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:elevation=\"5dp\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_support\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <ImageView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:adjustViewBounds=\"true\"\n        android:contentDescription=\"@null\"\n        android:src=\"@drawable/module_image_wechat_pay\" />\n\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_sync_card_data.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.sync.SyncCardDataActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:title=\"@string/module_sync_card_data\"\n            app:titleTextColor=\"@color/module_white\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <TextView\n        android:id=\"@+id/link\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_card_data_source\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\" />\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:id=\"@+id/til_version\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"8dp\"\n        android:hint=\"@string/module_version_code\"\n        app:endIconMode=\"clear_text\"\n        app:helperText=\"@string/module_version_code_helper_text\"\n        app:layout_constraintTop_toBottomOf=\"@id/link\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            android:id=\"@+id/version\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:inputType=\"number\"\n            />\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n\n    <RadioGroup\n        android:id=\"@+id/radio_group\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"8dp\"\n        app:layout_constraintTop_toBottomOf=\"@id/til_version\">\n\n        <RadioButton\n            android:id=\"@+id/chinese\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:checked=\"true\"\n            android:text=\"@string/module_chinese\" />\n\n        <RadioButton\n            android:id=\"@+id/english\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_english\" />\n    </RadioGroup>\n\n    <Button\n        android:id=\"@+id/sync\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"32dp\"\n        android:text=\"@string/module_sync\"\n        app:layout_constraintTop_toBottomOf=\"@id/radio_group\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.test.TestActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_test\"\n            app:titleTextColor=\"@color/module_white\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n    <TextView\n        android:id=\"@+id/create_record\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"生成对战记录\" />\n\n    <TextView\n        android:id=\"@+id/clear_log\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:padding=\"16dp\"\n        android:text=\"清空Power.log文件\" />\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.theme.ThemeActivity\">\n\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_theme\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <RadioGroup\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <RadioButton\n            android:id=\"@+id/system\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_theme_follow_system\"/>\n        <RadioButton\n            android:id=\"@+id/light\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_theme_light\"/>\n\n        <RadioButton\n            android:id=\"@+id/dark\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_theme_dark\"/>\n    </RadioGroup>\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_write_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.writeconfig.WriteConfigActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:title=\"@string/module_write_config_file\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n    <TextView\n        android:id=\"@+id/hint\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"16dp\"\n        android:text=\"@string/module_hint_write_config\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\" />\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:padding=\"16dp\"\n        app:layout_constraintBottom_toTopOf=\"@id/force_write\"\n        app:layout_constraintTop_toBottomOf=\"@id/hint\">\n\n        <TextView\n            android:id=\"@+id/content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n    </androidx.core.widget.NestedScrollView>\n\n    <com.google.android.material.switchmaterial.SwitchMaterial\n        android:id=\"@+id/force_write\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"24dp\"\n        android:text=\"@string/module_force_write\"\n        app:layout_constraintBottom_toTopOf=\"@id/write_in\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\" />\n\n\n    <Button\n        android:id=\"@+id/write_in\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"24dp\"\n        android:text=\"@string/module_write_in\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_zone_cards.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.zonecards.ZoneCardsActivity\">\n\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\"\n            app:title=\"@string/module_card_list\"\n            app:titleTextColor=\"@color/module_white\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\"\n        tools:listitem=\"@layout/module_item_zone_card\" />\n\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_activity_zone_events.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.zoneevents.ZoneEventsActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/app_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:navigationIcon=\"@drawable/module_baseline_arrow_back_white_24dp\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n    <com.google.android.material.button.MaterialButtonToggleGroup\n        android:id=\"@+id/toggle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        app:checkedButton=\"@id/slider_mode\"\n        app:layout_constraintTop_toBottomOf=\"@id/app_bar\"\n        app:singleSelection=\"true\">\n\n        <Button\n            android:id=\"@+id/slider_mode\"\n            style=\"?attr/materialButtonOutlinedStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n\n            android:text=\"@string/module_slider_mode\" />\n\n        <Button\n            android:id=\"@+id/list_mode\"\n            style=\"?attr/materialButtonOutlinedStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_list_mode\" />\n    </com.google.android.material.button.MaterialButtonToggleGroup>\n\n    <androidx.viewpager2.widget.ViewPager2\n        android:id=\"@+id/view_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/toggle\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_dialog_card_preview.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <ImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:adjustViewBounds=\"true\"\n        android:contentDescription=\"@null\"\n        tools:src=\"@mipmap/ic_launcher\" />\n\n</FrameLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_floating_window.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n\n    android:background=\"#33000000\"\n    android:orientation=\"vertical\"\n    tools:layout_width=\"@dimen/module_floating_window_width\">\n\n\n    <View\n        android:id=\"@+id/header\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"8dp\"\n        android:layout_marginTop=\"4dp\"\n        android:background=\"#f0f\"\n        android:visibility=\"gone\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <ImageView\n        android:id=\"@+id/drag\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@null\"\n        android:padding=\"8dp\"\n        android:src=\"@drawable/module_baseline_drag_handle_white_24dp\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/header\" />\n\n    <ImageView\n        android:id=\"@+id/close\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@null\"\n        android:padding=\"8dp\"\n        android:src=\"@drawable/module_baseline_clear_red_500_24dp\"\n        app:layout_constraintStart_toEndOf=\"@id/drag\"\n        app:layout_constraintTop_toTopOf=\"@id/drag\" />\n\n    <ImageView\n        android:id=\"@+id/hide\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@null\"\n        android:padding=\"8dp\"\n        android:src=\"@drawable/module_baseline_zoom_out_map_white_24dp\"\n        app:layout_constraintStart_toEndOf=\"@id/close\"\n        app:layout_constraintTop_toTopOf=\"@id/close\" />\n\n    <Spinner\n        android:id=\"@+id/spinner\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:backgroundTint=\"@color/module_white\"\n        android:spinnerMode=\"dropdown\"\n        app:layout_constraintBottom_toBottomOf=\"@id/close\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/close\" />\n\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/drag\"\n        tools:itemCount=\"100\"\n        tools:listitem=\"@layout/module_item_card\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/scale\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\">\n\n        <View\n            android:layout_width=\"4dp\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@color/module_white\"\n            app:layout_constraintEnd_toEndOf=\"parent\" />\n\n        <View\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"4dp\"\n            android:background=\"@color/module_white\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\" />\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_fragment_card_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.main.CardListFragment\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\" />\n\n</FrameLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_fragment_deck_battle_detail_deck_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"16dp\"\n            android:text=\"@string/module_cost\" />\n\n        <com.github.mikephil.charting.charts.BarChart\n            android:id=\"@+id/cost_chart\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"@dimen/module_chart_height\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"16dp\"\n            android:text=\"@string/module_attack\" />\n\n\n        <com.github.mikephil.charting.charts.BarChart\n            android:id=\"@+id/attack_chart\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"@dimen/module_chart_height\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"16dp\"\n            android:text=\"@string/module_mechanics\" />\n\n        <com.google.android.material.chip.ChipGroup\n            android:id=\"@+id/mechanics_chips\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"16dp\"\n            android:text=\"@string/module_race\" />\n\n        <com.google.android.material.chip.ChipGroup\n            android:id=\"@+id/race_chips\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n\n        <TextView\n            style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"16dp\"\n            android:text=\"@string/module_spell\" />\n\n        <com.google.android.material.chip.ChipGroup\n            android:id=\"@+id/spell_school_chips\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n    </LinearLayout>\n\n</androidx.core.widget.NestedScrollView>"
  },
  {
    "path": "module/src/main/res/layout/module_fragment_deck_battle_detail_summary.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.github.mikephil.charting.charts.PieChart\n        android:id=\"@+id/chart\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"@dimen/module_chart_height\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_fragment_graveyard.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.main.GraveyardFragment\">\n\n    <com.google.android.material.button.MaterialButtonToggleGroup\n        android:id=\"@+id/toggle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        app:checkedButton=\"@id/self\"\n        app:singleSelection=\"true\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <Button\n            android:id=\"@+id/self\"\n            style=\"?attr/materialButtonOutlinedStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_self\" />\n\n        <Button\n            android:id=\"@+id/opponent\"\n            style=\"?attr/materialButtonOutlinedStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/module_opponent\" />\n    </com.google.android.material.button.MaterialButtonToggleGroup>\n\n    <Spinner\n        android:id=\"@+id/sorted\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:entries=\"@array/module_sorted\"\n        android:padding=\"8dp\"\n        app:layout_constraintBottom_toBottomOf=\"@id/filter\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/filter\" />\n\n    <Button\n        android:id=\"@+id/filter\"\n        style=\"@style/Widget.MaterialComponents.Button.TextButton\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/module_filter\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n\n        app:layout_constraintTop_toBottomOf=\"@id/toggle\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/filter\"\n        tools:listitem=\"@layout/module_item_card\" />\n\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_fragment_list_mode.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".ui.zoneevents.ListModeFragment\">\n\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        tools:listitem=\"@layout/module_item_zone_event_list_mode\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\" />\n\n</FrameLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_fragment_loading.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ProgressBar\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n</FrameLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_fragment_opponent_hand_cards.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n        tools:listitem=\"@layout/module_item_opponent_hand_card\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_header_summary.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <TextView\n        android:id=\"@+id/all_win_rate\"\n        style=\"@style/TextAppearance.MaterialComponents.Headline1\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textStyle=\"bold\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:text=\"64\" />\n\n    <TextView\n        style=\"@style/TextAppearance.MaterialComponents.Headline3\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"%\"\n        app:layout_constraintBaseline_toBaselineOf=\"@id/all_win_rate\"\n        app:layout_constraintStart_toEndOf=\"@id/all_win_rate\" />\n\n    <TextView\n        android:id=\"@+id/all_count\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintBottom_toBottomOf=\"@id/win_count\"\n        app:layout_constraintEnd_toStartOf=\"@id/win_count\"\n        app:layout_constraintTop_toTopOf=\"@id/win_count\"\n        tools:text=\"总11\" />\n\n    <TextView\n        android:id=\"@+id/win_count\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"8dp\"\n        android:textColor=\"@color/module_game_state\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/all_win_rate\"\n        tools:text=\"胜：7\" />\n\n    <TextView\n        android:id=\"@+id/lost_count\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:enabled=\"false\"\n        android:textColor=\"@color/module_game_state\"\n        app:layout_constraintBottom_toBottomOf=\"@id/win_count\"\n        app:layout_constraintStart_toEndOf=\"@id/win_count\"\n        app:layout_constraintTop_toTopOf=\"@id/win_count\"\n        tools:text=\"负：4\" />\n\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <ImageView\n        android:id=\"@+id/image_tile\"\n        android:contentDescription=\"@null\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:foreground=\"#99000000\"\n        android:scaleType=\"centerCrop\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        tools:src=\"@drawable/module_image_tile\"\n        android:autoSizeTextType=\"uniform\"\n        app:layout_constraintStart_toEndOf=\"@id/cost\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintDimensionRatio=\"256:49\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n\n\n    <TextView\n        android:id=\"@+id/cost\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintDimensionRatio=\"1:1\"\n        android:autoSizeTextType=\"uniform\"\n        android:gravity=\"center\"\n        android:background=\"@color/module_rarity_common\"\n        android:textColor=\"@color/module_white\"\n        app:layout_constraintTop_toTopOf=\"@id/image_tile\"\n        app:layout_constraintBottom_toBottomOf=\"@id/image_tile\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        tools:text=\"2\" />\n\n    <TextView\n        android:id=\"@+id/name\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:textColor=\"@color/module_white\"\n        app:layout_constraintStart_toEndOf=\"@id/cost\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/image_tile\"\n        app:layout_constraintBottom_toBottomOf=\"@id/image_tile\"\n        android:autoSizeTextType=\"uniform\"\n        tools:text=\"始生研习\" />\n\n    <TextView\n        android:id=\"@+id/count\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        tools:text=\"2\"\n\n        app:layout_constraintDimensionRatio=\"1:1\"\n        android:background=\"#353535\"\n        android:gravity=\"center\"\n        android:textColor=\"@color/module_druid\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/image_tile\"\n        app:layout_constraintBottom_toBottomOf=\"@id/image_tile\"\n        android:autoSizeTextType=\"uniform\"/>\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_chip_filter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.chip.Chip xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    style=\"@style/Widget.MaterialComponents.Chip.Filter\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:checked=\"true\"\n    android:textSize=\"13sp\"\n    tools:text=\"@string/module_druid\">\n\n</com.google.android.material.chip.Chip>"
  },
  {
    "path": "module/src/main/res/layout/module_item_class_battle_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"horizontal\">\n\n    <TextView\n        android:id=\"@+id/value1\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center\"\n        android:paddingVertical=\"16dp\"\n        tools:text=\"战士\" />\n\n    <TextView\n        android:id=\"@+id/value2\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center\"\n        android:paddingVertical=\"16dp\"\n        tools:text=\"2\" />\n\n    <TextView\n        android:id=\"@+id/value3\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center\"\n        android:paddingVertical=\"16dp\"\n        tools:text=\"3\" />\n\n    <TextView\n        android:id=\"@+id/value4\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center\"\n        android:paddingVertical=\"16dp\"\n        tools:text=\"4\" />\n\n    <TextView\n        android:id=\"@+id/value5\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center\"\n        android:paddingVertical=\"16dp\"\n        tools:text=\"40%\" />\n\n\n</LinearLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_footer_with_fab.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"88dp\" />\n\n</FrameLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_opponent_hand_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingHorizontal=\"16dp\"\n    android:paddingVertical=\"8dp\">\n\n    <TextView\n        android:id=\"@+id/text\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"1\" />\n\n</FrameLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_record.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingHorizontal=\"16dp\"\n    android:paddingVertical=\"8dp\">\n\n    <ImageView\n        android:id=\"@+id/user_hero\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintDimensionRatio=\"1:1\"\n        app:layout_constraintWidth_percent=\"0.12\"\n        android:contentDescription=\"@null\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        tools:src=\"@drawable/module_image_round_warlock\" />\n\n    <TextView\n        android:id=\"@+id/vs\"\n        style=\"@style/TextAppearance.MaterialComponents.Headline6\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"0dp\"\n        android:gravity=\"center\"\n        android:maxLines=\"1\"\n        android:paddingHorizontal=\"16dp\"\n        android:text=\"VS\"\n        app:layout_constraintBottom_toBottomOf=\"@id/user_hero\"\n        app:layout_constraintStart_toEndOf=\"@id/user_hero\"\n        app:layout_constraintTop_toTopOf=\"@id/user_hero\" />\n\n    <ImageView\n        android:id=\"@+id/opponent_hero\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintDimensionRatio=\"1:1\"\n        app:layout_constraintWidth_percent=\"0.12\"\n        android:contentDescription=\"@null\"\n        app:layout_constraintStart_toEndOf=\"@id/vs\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        tools:src=\"@drawable/module_image_round_warlock\" />\n\n    <TextView\n        android:id=\"@+id/state\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:textColor=\"#00ff00\"\n        android:textColor=\"@color/module_game_state\"\n        app:layout_constraintBottom_toBottomOf=\"@id/user_hero\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/user_hero\"\n        tools:text=\"@string/module_win\" />\n\n    <TextView\n        android:id=\"@+id/game_type\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintEnd_toStartOf=\"@id/state\"\n        app:layout_constraintStart_toEndOf=\"@id/opponent_hero\"\n        app:layout_constraintTop_toTopOf=\"@id/opponent_hero\"\n        app:layout_constraintBottom_toTopOf=\"@id/date\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle2\"\n        tools:text=\"标准排名\"/>\n\n    <TextView\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintStart_toEndOf=\"@id/opponent_hero\"\n        app:layout_constraintEnd_toStartOf=\"@id/state\"\n        app:layout_constraintTop_toBottomOf=\"@id/game_type\"\n        android:gravity=\"center\"\n        android:autoSizeTextType=\"uniform\"\n        android:maxLines=\"1\"\n        android:id=\"@+id/date\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        tools:text=\"2022-12-12 09:00:00\"/>\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_summary_battle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:padding=\"16dp\">\n\n    <ImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@null\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:src=\"@drawable/module_image_round_warrior\" />\n\n    <TextView\n        android:id=\"@+id/name\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"16dp\"\n        app:layout_constraintStart_toEndOf=\"@id/image\"\n        app:layout_constraintTop_toBottomOf=\"@id/all_count\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:text=\"战士\" />\n\n    <TextView\n        android:id=\"@+id/all_count\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"@id/name\"\n        app:layout_constraintTop_toBottomOf=\"@id/name\"\n        tools:text=\"总：9\" />\n\n    <TextView\n        android:id=\"@+id/win_count\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:paddingHorizontal=\"8dp\"\n        android:textColor=\"@color/module_game_state\"\n        app:layout_constraintStart_toEndOf=\"@id/all_count\"\n        app:layout_constraintTop_toTopOf=\"@id/all_count\"\n        tools:text=\"胜：6\" />\n\n    <TextView\n        android:id=\"@+id/lost_count\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:enabled=\"false\"\n        android:paddingHorizontal=\"8dp\"\n        android:textColor=\"@color/module_game_state\"\n        app:layout_constraintStart_toEndOf=\"@id/win_count\"\n        app:layout_constraintTop_toTopOf=\"@id/all_count\"\n        tools:text=\"负：3\" />\n\n    <TextView\n        android:id=\"@+id/win_rate\"\n        style=\"@style/TextAppearance.MaterialComponents.Headline6\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintBottom_toBottomOf=\"@id/image\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/image\"\n        tools:text=\"64%\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <TextView\n        android:id=\"@+id/text\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingStart=\"16dp\"\n        android:paddingTop=\"8dp\"\n        android:paddingEnd=\"16dp\"\n        android:paddingBottom=\"8dp\" />\n\n</FrameLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_zone_card.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <ImageView\n        android:id=\"@+id/image_tile\"\n        android:contentDescription=\"@null\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:foreground=\"#99000000\"\n        android:scaleType=\"centerCrop\"\n        tools:src=\"@drawable/module_image_tile\"\n        app:layout_constraintEnd_toStartOf=\"@id/position\"\n        app:layout_constraintStart_toEndOf=\"@id/cost\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintDimensionRatio=\"256:30\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n\n\n    <TextView\n        android:id=\"@+id/cost\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintDimensionRatio=\"1:1\"\n        android:autoSizeTextType=\"uniform\"\n        android:gravity=\"center\"\n        android:padding=\"4dp\"\n        android:background=\"@color/module_rarity_common\"\n        android:textColor=\"@color/module_white\"\n        app:layout_constraintTop_toTopOf=\"@id/image_tile\"\n        app:layout_constraintBottom_toBottomOf=\"@id/image_tile\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        tools:text=\"2\" />\n\n    <TextView\n        android:id=\"@+id/name\"\n        style=\"@style/TextAppearance.MaterialComponents.Subtitle1\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:gravity=\"center_vertical\"\n        android:textColor=\"@color/module_white\"\n        app:layout_constraintStart_toEndOf=\"@id/cost\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/image_tile\"\n        android:padding=\"8dp\"\n        app:layout_constraintBottom_toBottomOf=\"@id/image_tile\"\n        tools:text=\"始生研习\" />\n\n    <TextView\n        android:id=\"@+id/position\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        tools:text=\"2\"\n        app:layout_constraintDimensionRatio=\"1:1\"\n        android:background=\"#353535\"\n        android:gravity=\"center\"\n        android:textColor=\"@color/module_white\"\n        android:padding=\"8dp\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/image_tile\"\n        app:layout_constraintBottom_toBottomOf=\"@id/image_tile\"\n        android:autoSizeTextType=\"uniform\"/>\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "module/src/main/res/layout/module_item_zone_event_list_mode.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.chip.ChipGroup xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n <com.google.android.material.chip.Chip\n     android:id=\"@+id/user_deck\"\n     android:layout_width=\"wrap_content\"\n     android:layout_height=\"wrap_content\"\n     tools:text=\"玩家牌库 10\"/>\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/user_hand\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"玩家手牌 10\"/>\n\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/user_play\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"玩家战场 10\"/>\n\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/user_graveyard\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"玩家墓地 10\"/>\n\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/user_secret\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"玩家奥秘 10\"/>\n\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/opponent_deck\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"对手牌库 10\"/>\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/opponent_hand\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"对手手牌 10\"/>\n\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/opponent_play\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"对手战场手牌 10\"/>\n\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/opponent_graveyard\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"对手墓地 10\"/>\n\n    <com.google.android.material.chip.Chip\n        android:id=\"@+id/opponent_secret\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        tools:text=\"对手奥秘 0\"/>\n</com.google.android.material.chip.ChipGroup>"
  },
  {
    "path": "module/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string-array name=\"module_sorted\">\n        <item>费用升序</item>\n        <item>费用降序</item>\n        <item>时间正序</item>\n        <item>时间倒序</item>\n    </string-array>\n\n    <string-array name=\"module_spinner\">\n        <item>@string/module_deck</item>\n        <item>@string/module_graveyard</item>\n        <item>@string/module_opponent_graveyard</item>\n\n    </string-array>\n</resources>"
  },
  {
    "path": "module/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"module_purple_200\">#FFBB86FC</color>\n    <color name=\"module_purple_500\">#FF6200EE</color>\n    <color name=\"module_purple_700\">#FF3700B3</color>\n    <color name=\"module_teal_200\">#FF03DAC5</color>\n    <color name=\"module_teal_700\">#FF018786</color>\n    <color name=\"module_black\">#FF000000</color>\n    <color name=\"module_white\">#FFFFFFFF</color>\n\n    <color name=\"module_mage\">#69CCF0</color>\n    <color name=\"module_warlock\">#9482C9</color>\n    <color name=\"module_druid\">#FF7D0A</color>\n    <color name=\"module_rogue\">#FFF569</color>\n    <color name=\"module_shaman\">#0070DE</color>\n    <color name=\"module_hunter\">#ABD473</color>\n    <color name=\"module_priest\">#FFFFFF</color>\n    <color name=\"module_warrior\">#C79C6E</color>\n    <color name=\"module_paladin\">#F58CBA</color>\n    <color name=\"module_demon_hunter\">#A330C9</color>\n    <color name=\"module_neutral\">#ccc</color>\n    <color name=\"module_death_knight\">#C41F3B</color>\n    <color name=\"module_divider\">#ccc</color>\n\n    <color name=\"module_rarity_common\">#828282</color>\n    <color name=\"module_rarity_rare\">#3d5375</color>\n    <color name=\"module_rarity_epic\">#604b7d</color>\n    <color name=\"module_rarity_legendary\">#845c2e</color>\n    <color name=\"module_loss\">#ff0000</color>\n    <color name=\"module_win\">#00ff00</color>\n\n    <color name=\"module_grey500\">#9E9E9E</color>\n\n</resources>"
  },
  {
    "path": "module/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <dimen name=\"module_chart_height\">300dp</dimen>\n    <dimen name=\"module_floating_window_width\">180dp</dimen>\n    <dimen name=\"module_floating_window_height\">300dp</dimen>\n    <dimen name=\"module_floating_window_header_height\">40dp</dimen>\n</resources>"
  },
  {
    "path": "module/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"module_app_name\">hs tracker</string>\n    <string name=\"module_set_permissions\">set permissions</string>\n    <string name=\"module_permissions_step_1\">1,open write external storage permission</string>\n    <string name=\"module_permissions_step_2\">1,allow access data dir</string>\n    <string name=\"module_permissions_step_3\">3,allow manage external storage</string>\n    <string name=\"module_next\">next</string>\n    <string name=\"module_permissions_step_4\">4,write config file</string>\n    <string name=\"module_loading\">loading</string>\n    <string name=\"module_sync_card_data\">Sync Card Data</string>\n    <string name=\"module_version_code\">Version Code</string>\n    <string name=\"module_default_version\">133122</string>\n    <string name=\"module_chinese\">Chinese</string>\n    <string name=\"module_english\">English</string>\n    <string name=\"module_sync\">sync</string>\n    <string name=\"module_syncing\">syncing</string>\n    <string name=\"module_code\">code</string>\n    <string name=\"module_default_deck_code\">AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA</string>\n    <string name=\"module_start\">start</string>\n    <string name=\"module_test\">test</string>\n    <string name=\"module_settings\">Settings</string>\n    <string name=\"module_deck\">deck</string>\n    <string name=\"module_graveyard\">graveyard</string>\n    <string name=\"module_opponent_graveyard\">opponent graveyard</string>\n    <string name=\"module_filter\">filter</string>\n    <string name=\"module_class\">class</string>\n    <string name=\"module_demon_hunter\">Demon Hunter</string>\n    <string name=\"module_druid\">Druid</string>\n    <string name=\"module_hunter\">Hunter</string>\n    <string name=\"module_mage\">Mage</string>\n    <string name=\"module_paladin\">Paladin</string>\n    <string name=\"module_priest\">Priest</string>\n    <string name=\"module_rogue\">Rogue</string>\n    <string name=\"module_shaman\">Shaman</string>\n    <string name=\"module_warlock\">Warlock</string>\n    <string name=\"module_warrior\">Warrior</string>\n    <string name=\"module_neutral\">Neutral</string>\n    <string name=\"module_death_knight\">Death Knight</string>\n    <string name=\"module_dream\">Dream</string>\n    <string name=\"module_whizbang\">Whizbang</string>\n    <string name=\"module_rarity_free\">Free</string>\n    <string name=\"module_rarity_common\">Common</string>\n    <string name=\"module_rarity_rare\">Rare</string>\n    <string name=\"module_rarity_epic\">Epic</string>\n    <string name=\"module_rarity_legendary\">Legendary</string>\n    <string name=\"module_rarity\">Rarity</string>\n    <string name=\"module_card_data_source\">Source from:https://hearthstonejson.com</string>\n    <string name=\"module_hint\">Hint</string>\n    <string name=\"module_sync_success\">Sync success</string>\n    <string name=\"module_done\">Done</string>\n    <string name=\"module_author_email\">913918146@qq.com</string>\n    <string name=\"module_contact_author\">Contact Author</string>\n    <string name=\"module_language\">Language</string>\n    <string name=\"module_theme\">Theme</string>\n    <string name=\"module_theme_light\">Light</string>\n    <string name=\"module_theme_dark\">Dark</string>\n    <string name=\"module_theme_follow_system\">Follow System</string>\n    <string name=\"module_code_parser\">Deck Code Parser</string>\n    <string name=\"module_diagnose\">Diagnose</string>\n    <string name=\"module_check_config_file\">Check Config File</string>\n    <string name=\"module_write_config_file\">Write Config File</string>\n    <string name=\"module_write_in\">Write In</string>\n    <string name=\"module_force_write\">Force Write</string>\n    <string name=\"module_hint_write_config\">In order to read the log, we will write the contents to the log.config file in the Hearthstone directory</string>\n    <string name=\"module_write_config_failed\">Write Config Failed</string>\n    <string name=\"module_rewrite_config_file\">Rewrite Config File</string>\n    <string name=\"module_self\">Self</string>\n    <string name=\"module_opponent\">Opponent</string>\n    <string name=\"module_race\">Race</string>\n    <string name=\"module_minion\">Minion</string>\n    <string name=\"module_spell\">Spell</string>\n    <string name=\"module_weapon\">Weapon</string>\n    <string name=\"module_hero\">Hero</string>\n    <string name=\"module_mechanics\">Mechanics</string>\n    <string name=\"module_pirate\">Pirate</string>\n    <string name=\"module_mechanical\">Mechanical</string>\n    <string name=\"module_dragon\">Dragon</string>\n    <string name=\"module_beast\">Beast</string>\n    <string name=\"module_murloc\">Murloc</string>\n    <string name=\"module_demon\">Demon</string>\n    <string name=\"module_totem\">Totem</string>\n    <string name=\"module_elemental\">Elemental</string>\n    <string name=\"module_quilboar\">Quilboar</string>\n    <string name=\"module_all\">All</string>\n    <string name=\"module_arcane\">Arcane</string>\n    <string name=\"module_frost\">Frost</string>\n    <string name=\"module_fire\">Fire</string>\n    <string name=\"module_nature\">Nature</string>\n    <string name=\"module_shadow\">Shadow</string>\n    <string name=\"module_holy\">Holy</string>\n    <string name=\"module_Fel\">Fel</string>\n    <string name=\"module_version_code_helper_text\">No input means to get the latest</string>\n    <string name=\"module_records\">Records</string>\n    <string name=\"module_win\">Win</string>\n    <string name=\"module_loss\">Loss</string>\n\n    <string name=\"module_unknown\">Unknown</string>\n    <string name=\"module_standard\">Standard</string>\n    <string name=\"module_wild\">Wild</string>\n    <string name=\"module_classic\">Classic</string>\n    <string name=\"module_ranked\">Ranked</string>\n    <string name=\"module_casual\">Casual</string>\n    <string name=\"module_previous\">Previous</string>\n    <string name=\"module_slider_mode\">Slider Mode</string>\n    <string name=\"module_list_mode\">List Mode</string>\n\n    <string name=\"module_save_log_file\">Save Log File</string>\n    <string name=\"module_card_list\">Card List</string>\n    <string name=\"module_naga\">Naga</string>\n    <string name=\"module_summary\">Summary</string>\n    <string name=\"module_by_class\">By Class</string>\n    <string name=\"module_by_deck\">By Deck</string>\n    <string name=\"module_view_all_games\">View All Games</string>\n    <string name=\"module_data_transfer\">Migrate Data</string>\n    <string name=\"module_title_migrate_date_to_another_phone\">Migrate data to another phone</string>\n    <string name=\"module_title_migrate_data_to_this_phone\">Migrate data from another phone to this phone</string>\n    <string name=\"module_receive\">Receive</string>\n    <string name=\"module_port\">Port</string>\n    <string name=\"module_pie_chart\">Pie Chart</string>\n    <string name=\"module_all_win_rate\">Overall Win Rate</string>\n    <string name=\"module_first_hand_rate\">First Hand Rate</string>\n    <string name=\"module_first_hand_percent\">First Hand Percent</string>\n    <string name=\"module_second_hand_rate\">Second Hand Rate</string>\n    <string name=\"module_class_distribution\">Class Distribution</string>\n    <string name=\"module_first_hand\">First Hand</string>\n    <string name=\"module_second_hand\">Second Hand</string>\n    <string name=\"module_class_battle_detail\">Class Battle Detail</string>\n    <string name=\"module_opponent_class\">Opponent Class</string>\n    <string name=\"module_times\">Times</string>\n    <string name=\"module_win_rate\">Win Rate</string>\n    <string name=\"module_deck_battle_detail\">Deck Battle Detail</string>\n    <string name=\"module_cost\">Cost</string>\n    <string name=\"module_attack\">Attack</string>\n    <string name=\"module_record\">Records</string>\n    <string name=\"module_deck_detail\">Deck Detail</string>\n    <string name=\"module_opponent_hand_card\">Opponent hand card</string>\n    <string name=\"module_tracker_style\">Tracker Style</string>\n\n    <string name=\"module_small_window\">Small Window</string>\n    <string name=\"module_floating_window\">Floating Window</string>\n    <string name=\"module_floating_track\">Floating Window</string>\n    <string name=\"module_github_address\">https://github.com/keluokeda/hs_tracker</string>\n    <string name=\"module_support\">Support</string>\n</resources>"
  },
  {
    "path": "module/src/main/res/values/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n\n    <color name=\"module_filter_activity_color\">#ddd</color>\n\n    <!-- Base application theme. -->\n    <style name=\"module_Theme.Hs_tracker\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <!-- Primary brand color. -->\n        <item name=\"colorPrimary\">@color/module_purple_500</item>\n        <item name=\"colorPrimaryVariant\">@color/module_purple_700</item>\n        <item name=\"colorOnPrimary\">@color/module_white</item>\n        <!-- Secondary brand color. -->\n        <item name=\"colorSecondary\">@color/module_teal_200</item>\n        <item name=\"colorSecondaryVariant\">@color/module_teal_700</item>\n        <item name=\"colorOnSecondary\">@color/module_black</item>\n        <!-- Status bar color. -->\n        <item name=\"android:statusBarColor\" tools:targetApi=\"l\">?attr/colorPrimaryVariant</item>\n    </style>\n\n\n    <style name=\"module_Theme.Hs_tracker.Main\" parent=\"module_Theme.Hs_tracker\">\n\n    </style>\n\n    <style name=\"module_Theme.Splash\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <item name=\"android:windowTranslucentNavigation\">true</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:ignore=\"NewApi\">shortEdges</item>\n        <item name=\"android:windowFullscreen\">true</item>\n        <item name=\"android:windowBackground\">@drawable/module_bg_splash</item>\n    </style>\n</resources>"
  },
  {
    "path": "module/src/main/res/values-night/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <color name=\"module_filter_activity_color\">#111</color>\n\n    <!-- Base application theme. -->\n    <style name=\"module_Theme.Hs_tracker\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <!-- Primary brand color. -->\n        <!-- Primary brand color. -->\n        <item name=\"colorPrimary\">@color/module_purple_200</item>\n        <item name=\"colorPrimaryVariant\">@color/module_purple_700</item>\n        <item name=\"colorOnPrimary\">@color/module_black</item>\n        <!-- Secondary brand color. -->\n        <item name=\"colorSecondary\">@color/module_teal_200</item>\n        <item name=\"colorSecondaryVariant\">@color/module_teal_200</item>\n        <item name=\"colorOnSecondary\">@color/module_black</item>\n        <!-- Status bar color. -->\n        <item name=\"android:statusBarColor\" tools:targetApi=\"l\">@color/module_black</item>\n    </style>\n\n    <style name=\"module_Theme.Hs_tracker.Main\" parent=\"module_Theme.Hs_tracker\">\n\n        <item name=\"android:windowBackground\">#66000000</item>\n    </style>\n</resources>"
  },
  {
    "path": "module/src/main/res/values-zh-rCN/strings.xml",
    "content": "<resources>\n    <string name=\"module_app_name\">炉石记牌器</string>\n    <string name=\"module_set_permissions\">设置权限</string>\n    <string name=\"module_permissions_step_1\">1，打开存储权限</string>\n    <string name=\"module_permissions_step_2\">1，允许访问data目录</string>\n    <string name=\"module_permissions_step_3\">3，允许管理外部存储</string>\n    <string name=\"module_next\">下一步</string>\n    <string name=\"module_permissions_step_4\">4，写入配置文件</string>\n    <string name=\"module_loading\">加载中</string>\n    <string name=\"module_sync_card_data\">同步卡牌数据</string>\n    <string name=\"module_version_code\">版本号</string>\n    <string name=\"module_default_version\">133122</string>\n    <string name=\"module_chinese\">中文</string>\n    <string name=\"module_english\">英文</string>\n    <string name=\"module_sync\">同步</string>\n    <string name=\"module_syncing\">同步中</string>\n    <string name=\"module_code\">代码</string>\n    <string name=\"module_default_deck_code\">AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA</string>\n    <string name=\"module_start\">开始</string>\n    <string name=\"module_test\">测试</string>\n    <string name=\"module_settings\">设置</string>\n    <string name=\"module_deck\">牌库</string>\n    <string name=\"module_graveyard\">墓地</string>\n    <string name=\"module_opponent_graveyard\">对手墓地</string>\n    <string name=\"module_filter\">过滤</string>\n    <string name=\"module_class\">职业</string>\n    <string name=\"module_demon_hunter\">恶魔猎手</string>\n    <string name=\"module_druid\">德鲁伊</string>\n    <string name=\"module_hunter\">猎人</string>\n    <string name=\"module_mage\">法师</string>\n    <string name=\"module_paladin\">圣骑士</string>\n    <string name=\"module_priest\">牧师</string>\n    <string name=\"module_rogue\">潜行者</string>\n    <string name=\"module_shaman\">萨满</string>\n    <string name=\"module_warlock\">术士</string>\n    <string name=\"module_warrior\">战士</string>\n    <string name=\"module_neutral\">中立</string>\n    <string name=\"module_death_knight\">死亡骑士</string>\n    <string name=\"module_dream\">梦境</string>\n    <string name=\"module_whizbang\">威兹班</string>\n    <string name=\"module_rarity_free\">免费</string>\n    <string name=\"module_rarity_common\">普通</string>\n    <string name=\"module_rarity_rare\">稀有</string>\n    <string name=\"module_rarity_epic\">史诗</string>\n    <string name=\"module_rarity_legendary\">传说</string>\n    <string name=\"module_rarity\">品质</string>\n    <string name=\"module_card_data_source\">数据来源：https://hearthstonejson.com</string>\n    <string name=\"module_hint\">提示</string>\n    <string name=\"module_sync_success\">同步成功</string>\n    <string name=\"module_done\">完成</string>\n    <string name=\"module_author_email\">913918146@qq.com</string>\n    <string name=\"module_contact_author\">联系作者</string>\n    <string name=\"module_language\">语言</string>\n    <string name=\"module_theme\">主题</string>\n    <string name=\"module_theme_light\">浅色</string>\n    <string name=\"module_theme_dark\">深色</string>\n    <string name=\"module_theme_follow_system\">跟随系统</string>\n    <string name=\"module_code_parser\">卡组代码解析</string>\n    <string name=\"module_diagnose\">诊断</string>\n    <string name=\"module_check_config_file\">检查Config文件</string>\n    <string name=\"module_write_config_file\">写入配置文件</string>\n    <string name=\"module_write_in\">写入</string>\n    <string name=\"module_force_write\">强制写入</string>\n    <string name=\"module_hint_write_config\">为了读取日志，我们将把以下内容写入到炉石目录的log.config文件中</string>\n    <string name=\"module_write_config_failed\">写入配置失败</string>\n    <string name=\"module_rewrite_config_file\">重写配置文件</string>\n    <string name=\"module_self\">自己</string>\n    <string name=\"module_opponent\">对手</string>\n    <string name=\"module_race\">种族</string>\n    <string name=\"module_minion\">随从</string>\n    <string name=\"module_spell\">法术</string>\n    <string name=\"module_weapon\">武器</string>\n    <string name=\"module_hero\">英雄</string>\n    <string name=\"module_mechanics\">类型</string>\n    <string name=\"module_pirate\">海盗</string>\n    <string name=\"module_mechanical\">机械</string>\n    <string name=\"module_dragon\">龙</string>\n    <string name=\"module_beast\">野兽</string>\n    <string name=\"module_murloc\">鱼人</string>\n    <string name=\"module_demon\">恶魔</string>\n    <string name=\"module_totem\">图腾</string>\n    <string name=\"module_elemental\">元素</string>\n    <string name=\"module_quilboar\">野猪人</string>\n    <string name=\"module_all\">全部</string>\n    <string name=\"module_arcane\">奥术</string>\n    <string name=\"module_frost\">冰霜</string>\n    <string name=\"module_fire\">火焰</string>\n    <string name=\"module_nature\">自然</string>\n    <string name=\"module_shadow\">暗影</string>\n    <string name=\"module_holy\">神圣</string>\n    <string name=\"module_Fel\">邪能</string>\n    <string name=\"module_version_code_helper_text\">不输入表示获取最新的</string>\n    <string name=\"module_records\">战绩</string>\n    <string name=\"module_win\">胜利</string>\n    <string name=\"module_loss\">失败</string>\n\n    <string name=\"module_unknown\">未知</string>\n    <string name=\"module_standard\">标准</string>\n    <string name=\"module_wild\">狂野</string>\n    <string name=\"module_classic\">经典</string>\n    <string name=\"module_ranked\">排名</string>\n    <string name=\"module_casual\">休闲</string>\n    <string name=\"module_previous\">上一步</string>\n    <string name=\"module_slider_mode\">进度条模式</string>\n    <string name=\"module_list_mode\">列表模式</string>\n    <string name=\"module_save_log_file\">保存日志文件</string>\n    <string name=\"module_card_list\">卡牌列表</string>\n    <string name=\"module_naga\">娜迦</string>\n    <string name=\"module_summary\">总览</string>\n    <string name=\"module_by_class\">按职业</string>\n    <string name=\"module_by_deck\">按卡组</string>\n    <string name=\"module_view_all_games\">查看所有对局</string>\n    <string name=\"module_data_transfer\">迁移数据</string>\n    <string name=\"module_title_migrate_date_to_another_phone\">迁移数据到另一台手机</string>\n    <string name=\"module_title_migrate_data_to_this_phone\">从另一台手机迁移数据到本手机</string>\n    <string name=\"module_receive\">接收</string>\n    <string name=\"module_port\">端口</string>\n    <string name=\"module_pie_chart\">饼状图</string>\n    <string name=\"module_all_win_rate\">总体胜率</string>\n    <string name=\"module_first_hand_rate\">先手胜率</string>\n    <string name=\"module_first_hand_percent\">先手概率</string>\n    <string name=\"module_second_hand_rate\">后手胜率</string>\n    <string name=\"module_class_distribution\">职业分布</string>\n    <string name=\"module_first_hand\">先手</string>\n    <string name=\"module_second_hand\">后手</string>\n    <string name=\"module_class_battle_detail\">职业对战明细</string>\n    <string name=\"module_opponent_class\">对手职业</string>\n    <string name=\"module_times\">场数</string>\n    <string name=\"module_win_rate\">胜率</string>\n    <string name=\"module_deck_battle_detail\">卡组对战明细</string>\n    <string name=\"module_cost\">费用</string>\n    <string name=\"module_attack\">攻击力</string>\n    <string name=\"module_record\">记录</string>\n    <string name=\"module_deck_detail\">卡组详情</string>\n    <string name=\"module_opponent_hand_card\">对手手牌</string>\n    <string name=\"module_tracker_style\">记牌器样式</string>\n    <string name=\"module_small_window\">小窗口</string>\n    <string name=\"module_floating_window\">悬浮窗</string>\n    <string name=\"module_floating_track\">悬浮窗记牌</string>\n    <string name=\"module_github_address\">https://github.com/keluokeda/hs_tracker</string>\n    <string name=\"module_support\">赞助</string>\n\n</resources>"
  },
  {
    "path": "module/src/test/java/com/ke/hs_tracker/module/ExampleUnitTest.kt",
    "content": "package com.ke.hs_tracker.module\n\nimport com.ke.hs_tracker.module.db.CardClassesConvert\nimport com.ke.hs_tracker.module.entity.CardClass\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n\n    @Test\n    fun testCardClassesConvert() {\n        val convert = CardClassesConvert()\n\n        val source = listOf(\n            CardClass.Hunter,\n            CardClass.Druid,\n            CardClass.Mage\n        )\n\n        val l = convert.classesToLong(source)\n\n        val list = convert.longToClasses(l)\n        val size = list.size\n\n        assertEquals(source, list)\n    }\n}"
  },
  {
    "path": "settings.gradle",
    "content": "pluginManagement {\n    repositories {\n        gradlePluginPortal()\n        google()\n        mavenCentral()\n        maven { url 'https://maven.aliyun.com/nexus/content/groups/public/'}\n        maven { url'https://maven.aliyun.com/repository/public/' }\n        maven { url'https://maven.aliyun.com/repository/google/' }\n        maven { url'https://maven.aliyun.com/repository/jcenter/' }\n        maven { url'https://maven.aliyun.com/repository/central/'}\n\n    }\n}\n\n//enableFeaturePreview('VERSION_CATALOGS')\ndependencyResolutionManagement {\n    versionCatalogs {\n        libs {\n            from(files(\"libs.versions.toml\"))\n        }\n    }\n}\n\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n        maven { url 'https://maven.aliyun.com/nexus/content/groups/public/'}\n        maven { url'https://maven.aliyun.com/repository/public/' }\n        maven { url'https://maven.aliyun.com/repository/google/' }\n        maven { url'https://maven.aliyun.com/repository/jcenter/' }\n        maven { url'https://maven.aliyun.com/repository/central/'}\n    }\n}\nrootProject.name = \"hs_tracker\"\ninclude ':app'\ninclude ':module'\ninclude ':writer'\ninclude ':simulator'\ninclude ':shared'\n"
  },
  {
    "path": "shared/.gitignore",
    "content": "/build"
  },
  {
    "path": "shared/build.gradle",
    "content": "plugins {\n    id 'java-library'\n    id 'org.jetbrains.kotlin.jvm'\n    id 'kotlin-kapt'\n\n}\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_17\n    targetCompatibility = JavaVersion.VERSION_17\n}\n\ndependencies {\n\n\n\n    implementation(libs.moshi)\n    kapt(libs.moshi.codegen)\n\n   \n}"
  },
  {
    "path": "shared/src/main/java/com/ke/hs/shared/entity/CardType.kt",
    "content": "package com.ke.hs.shared.entity\n\nimport com.squareup.moshi.FromJson\nimport com.squareup.moshi.ToJson\n\n\nenum class CardType {\n\n\n    /**\n     * 英雄\n     */\n    Hero,\n\n    /**\n     * 英雄技能\n     */\n    HeroPower,\n\n\n    /**\n     *衍生牌\n     */\n    Enchantment,\n\n    /**\n     * 法术\n     */\n    Spell,\n\n    /**\n     * 随从\n     */\n    Minion,\n\n    /**\n     * 武器\n     */\n    Weapon,\n\n\n    None\n}\n\nclass CardTypeAdapter {\n\n    @FromJson\n    fun fromJson(value: String): CardType {\n\n        return CardType.values()\n            .find { it.name.equals(value.replace(\"_\", \"\"), true) } ?: CardType.None\n\n    }\n\n    @ToJson\n    fun toJson(cardType: CardType) = cardType.name.uppercase()\n}"
  },
  {
    "path": "simulator/.gitignore",
    "content": "/build"
  },
  {
    "path": "simulator/build.gradle",
    "content": "plugins {\n    id 'com.android.library'\n    id 'org.jetbrains.kotlin.android'\n    id 'kotlin-kapt'\n    id 'dagger.hilt.android.plugin'\n    id 'kotlin-parcelize'\n}\n\nandroid {\n    compileSdk libs.versions.compilesdk.get().toInteger()\n    defaultConfig {\n        minSdk libs.versions.minsdk.get().toInteger()\n        targetSdk libs.versions.targetsdk.get().toInteger()\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_17\n        targetCompatibility JavaVersion.VERSION_17\n    }\n\n\n    resourcePrefix 'simulator_'\n\n    buildFeatures {\n        viewBinding = true\n    }\n    namespace 'com.ke.hs.simulator'\n}\n\ndependencies {\n\n    implementation project(path: ':module')\n\n    implementation 'androidx.core:core-ktx:1.7.0'\n    implementation 'androidx.appcompat:appcompat:1.3.0'\n    implementation 'com.google.android.material:material:1.4.0'\n    testImplementation 'junit:junit:4.13.2'\n    androidTestImplementation 'androidx.test.ext:junit:1.1.3'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'\n\n    implementation(libs.core.ktx)\n    implementation(libs.appcompat)\n    implementation(libs.material)\n    implementation(libs.constraint.layout)\n    implementation(libs.fragment.ktx)\n    implementation(libs.activity)\n    implementation(libs.lifecycle.viewmodel.ktx)\n    implementation(libs.lifecycle.livedata.ktx)\n    implementation(libs.lifecycle.runtime.ktx)\n    implementation(libs.support.v4)\n    implementation(libs.logger)\n\n\n    implementation(libs.hilt.android)\n    kapt(libs.hilt.compiler)\n\n    implementation(libs.glide)\n    kapt(libs.glide.compiler)\n\n    implementation(libs.ke.mvvm)\n    implementation(libs.adapter.helper)\n\n    implementation(libs.retrofit)\n    implementation(libs.retrofit.converter.moshi)\n    implementation(libs.moshi)\n    kapt(libs.moshi.codegen)\n\n    implementation(libs.hi.binding)\n    implementation(libs.mmkv)\n\n}"
  },
  {
    "path": "simulator/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "simulator/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "simulator/src/androidTest/java/com/ke/hs/simulator/ExampleInstrumentedTest.kt",
    "content": "package com.ke.hs.simulator\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.ke.hs.simulator.test\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "simulator/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest>\n\n</manifest>"
  },
  {
    "path": "simulator/src/main/java/com/ke/hs/simulator/cards/base/HeroCard.kt",
    "content": "package com.ke.hs.simulator.cards.base\n\ninterface HeroCard : ICard {\n    /**\n     * 护甲\n     */\n    val armor: Int\n\n    var health: Int\n}"
  },
  {
    "path": "simulator/src/main/java/com/ke/hs/simulator/cards/base/ICard.kt",
    "content": "package com.ke.hs.simulator.cards.base\n\nimport androidx.annotation.StringRes\nimport com.ke.hs_tracker.module.entity.CardClass\nimport com.ke.hs_tracker.module.entity.Rarity\n\ninterface ICard {\n\n    val id: String\n\n    val dbfId: Int\n\n    /**\n     * 费用\n     */\n    var cost: Int\n\n    /**\n     * 职业\n     */\n    val cardClass: CardClass\n\n    /**\n     * 名称\n     */\n    val name: Int\n\n    /**\n     * 品质 衍生牌没有\n     */\n    val rarity: Rarity?\n}"
  },
  {
    "path": "simulator/src/main/java/com/ke/hs/simulator/cards/base/MinionCard.kt",
    "content": "package com.ke.hs.simulator.cards.base\n\nimport com.ke.hs_tracker.module.entity.Race\n\ninterface MinionCard : ICard {\n\n    /**\n     * 攻击力\n     */\n    var attack: Int\n\n    /**\n     * 生命值\n     */\n    var health: Int\n\n    /**\n     * 种族\n     */\n    val race: Race?\n}"
  },
  {
    "path": "simulator/src/main/java/com/ke/hs/simulator/cards/base/SpellCard.kt",
    "content": "package com.ke.hs.simulator.cards.base\n\nimport com.ke.hs_tracker.module.entity.SpellSchool\n\ninterface SpellCard : ICard {\n\n    /**\n     * 法术类型\n     */\n    val spellSchool: SpellSchool?\n}"
  },
  {
    "path": "simulator/src/main/java/com/ke/hs/simulator/cards/base/WeaponCard.kt",
    "content": "package com.ke.hs.simulator.cards.base\n\ninterface WeaponCard {\n\n    /**\n     * 攻击力\n     */\n    var attack: Int\n\n    /**\n     * 耐久\n     */\n    var durability: Int\n}"
  },
  {
    "path": "simulator/src/test/java/com/ke/hs/simulator/ExampleUnitTest.kt",
    "content": "package com.ke.hs.simulator\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  },
  {
    "path": "writer/.gitignore",
    "content": "/build"
  },
  {
    "path": "writer/build.gradle",
    "content": "plugins {\n    id 'com.android.application'\n    id 'org.jetbrains.kotlin.android'\n    id 'kotlin-kapt'\n    id 'dagger.hilt.android.plugin'\n    id 'kotlin-parcelize'\n}\n\nandroid {\n    compileSdk libs.versions.compilesdk.get().toInteger()\n\n    defaultConfig {\n        applicationId \"com.ke.hs.writer\"\n        minSdk libs.versions.minsdk.get().toInteger()\n        targetSdk libs.versions.targetsdk.get().toInteger()\n        versionCode 1\n        versionName \"1.0\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_17\n        targetCompatibility JavaVersion.VERSION_17\n    }\n\n    namespace 'com.ke.hs.writer'\n}\n\ndependencies {\n\n    implementation project(path: ':module')\n\n    testImplementation 'junit:junit:4.+'\n    androidTestImplementation 'androidx.test.ext:junit:1.1.2'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'\n\n    implementation(libs.core.ktx)\n    implementation(libs.appcompat)\n    implementation(libs.material)\n    implementation(libs.constraint.layout)\n    implementation(libs.fragment.ktx)\n    implementation(libs.activity)\n    implementation(libs.lifecycle.viewmodel.ktx)\n    implementation(libs.lifecycle.livedata.ktx)\n    implementation(libs.lifecycle.runtime.ktx)\n    implementation(libs.support.v4)\n    implementation(libs.logger)\n\n\n    implementation(libs.hilt.android)\n    kapt(libs.hilt.compiler)\n\n    implementation(libs.ke.mvvm)\n\n    implementation(libs.moshi)\n    kapt(libs.moshi.codegen)\n}"
  },
  {
    "path": "writer/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "writer/release/output-metadata.json",
    "content": "{\n  \"version\": 3,\n  \"artifactType\": {\n    \"type\": \"APK\",\n    \"kind\": \"Directory\"\n  },\n  \"applicationId\": \"com.ke.hs.writer\",\n  \"variantName\": \"release\",\n  \"elements\": [\n    {\n      \"type\": \"SINGLE\",\n      \"filters\": [],\n      \"attributes\": [],\n      \"versionCode\": 1,\n      \"versionName\": \"1.0\",\n      \"outputFile\": \"writer-release.apk\"\n    }\n  ],\n  \"elementType\": \"File\"\n}"
  },
  {
    "path": "writer/src/androidTest/java/com/ke/hs/writer/ExampleInstrumentedTest.kt",
    "content": "package com.ke.hs.writer\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.ke.hs.writer\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "writer/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest>\n\n\n</manifest>"
  },
  {
    "path": "writer/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "writer/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>"
  },
  {
    "path": "writer/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "writer/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "writer/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n</resources>"
  },
  {
    "path": "writer/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">writer</string>\n</resources>"
  },
  {
    "path": "writer/src/main/res/values/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.Hs_tracker\" parent=\"Theme.MaterialComponents.DayNight.DarkActionBar\">\n        <!-- Primary brand color. -->\n        <item name=\"colorPrimary\">@color/purple_500</item>\n        <item name=\"colorPrimaryVariant\">@color/purple_700</item>\n        <item name=\"colorOnPrimary\">@color/white</item>\n        <!-- Secondary brand color. -->\n        <item name=\"colorSecondary\">@color/teal_200</item>\n        <item name=\"colorSecondaryVariant\">@color/teal_700</item>\n        <item name=\"colorOnSecondary\">@color/black</item>\n        <!-- Status bar color. -->\n        <item name=\"android:statusBarColor\" tools:targetApi=\"l\">?attr/colorPrimaryVariant</item>\n        <!-- Customize your theme here. -->\n    </style>\n</resources>"
  },
  {
    "path": "writer/src/main/res/values-night/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.Hs_tracker\" parent=\"Theme.MaterialComponents.DayNight.DarkActionBar\">\n        <!-- Primary brand color. -->\n        <item name=\"colorPrimary\">@color/purple_200</item>\n        <item name=\"colorPrimaryVariant\">@color/purple_700</item>\n        <item name=\"colorOnPrimary\">@color/black</item>\n        <!-- Secondary brand color. -->\n        <item name=\"colorSecondary\">@color/teal_200</item>\n        <item name=\"colorSecondaryVariant\">@color/teal_200</item>\n        <item name=\"colorOnSecondary\">@color/black</item>\n        <!-- Status bar color. -->\n        <item name=\"android:statusBarColor\" tools:targetApi=\"l\">?attr/colorPrimaryVariant</item>\n        <!-- Customize your theme here. -->\n    </style>\n</resources>"
  },
  {
    "path": "writer/src/test/java/com/ke/hs/writer/ExampleUnitTest.kt",
    "content": "package com.ke.hs.writer\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  }
]