Repository: keluokeda/hs_tracker Branch: master Commit: b4c42c2ecf38 Files: 279 Total size: 562.9 KB Directory structure: gitextract_40etbzh8/ ├── .gitignore ├── 123456 ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── release/ │ │ ├── app-1-3-1.apk │ │ └── output-metadata.json │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── ke/ │ └── hs_tracker/ │ └── app/ │ ├── App.kt │ └── MainActivity.kt ├── build.gradle ├── core/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── ke/ │ └── hs_tracker/ │ └── core/ │ ├── api/ │ │ └── HearthStoneJsonApi.kt │ ├── entity/ │ │ ├── BlockType.kt │ │ ├── Card.kt │ │ ├── CardClass.kt │ │ ├── CurrentDeck.kt │ │ ├── Entity.kt │ │ ├── GameCardType.kt │ │ ├── InsertStackResult.kt │ │ ├── LogType.kt │ │ ├── Mechanics.kt │ │ ├── NestedTag.kt │ │ ├── PowerTag.kt │ │ └── Zone.kt │ ├── extensions.kt │ └── parser/ │ ├── BlockTagStack.kt │ ├── DeckFileObserver.kt │ ├── PowerFileObserver.kt │ └── PowerParser.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── libs.versions.toml ├── module/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ ├── schemas/ │ │ └── com.ke.hs_tracker.module.db.Database/ │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ └── 4.json │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── ke/ │ │ └── hs_tracker/ │ │ └── module/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ ├── Decks.log │ │ │ ├── Power.log │ │ │ └── log.config │ │ ├── java/ │ │ │ └── com/ │ │ │ └── ke/ │ │ │ └── hs_tracker/ │ │ │ └── module/ │ │ │ ├── MainApplication.kt │ │ │ ├── api/ │ │ │ │ └── HearthStoneJsonApi.kt │ │ │ ├── data/ │ │ │ │ └── PreferenceStorage.kt │ │ │ ├── db/ │ │ │ │ ├── CardClassesConvert.kt │ │ │ │ ├── CardDao.kt │ │ │ │ ├── Database.kt │ │ │ │ ├── Game.kt │ │ │ │ ├── GameDao.kt │ │ │ │ ├── MechanicsListConvert.kt │ │ │ │ ├── ZonePositionChangedEvent.kt │ │ │ │ └── ZonePositionChangedEventDao.kt │ │ │ ├── di/ │ │ │ │ ├── CoroutinesModule.kt │ │ │ │ ├── CoroutinesQualifiers.kt │ │ │ │ ├── LogFileDirQualifiers.kt │ │ │ │ └── Module.kt │ │ │ ├── domain/ │ │ │ │ ├── ClearCardTableUseCase.kt │ │ │ │ ├── GetAllCardUseCase.kt │ │ │ │ ├── GetCardListUseCase.kt │ │ │ │ ├── GetDatabaseCardCountUseCase.kt │ │ │ │ ├── GetLocalLogDirUseCase.kt │ │ │ │ ├── GetRealLogDirUseCase.kt │ │ │ │ ├── GetSaveLogFileEnableUseCase.kt │ │ │ │ ├── InsertCardListToDatabaseUseCase.kt │ │ │ │ ├── ParseDeckCodeUseCase.kt │ │ │ │ ├── SaveLogFileUseCase.kt │ │ │ │ ├── SetSaveLogFileEnableUseCase.kt │ │ │ │ └── WriteLogConfigFileUseCase.kt │ │ │ ├── entity/ │ │ │ │ ├── BlockType.kt │ │ │ │ ├── Card.kt │ │ │ │ ├── CardBean.kt │ │ │ │ ├── CardClass.kt │ │ │ │ ├── CardType.kt │ │ │ │ ├── CurrentDeck.kt │ │ │ │ ├── Entity.kt │ │ │ │ ├── EntityWithPayload.kt │ │ │ │ ├── EnumMoshiAdapter.kt │ │ │ │ ├── FormatType.kt │ │ │ │ ├── GameCardType.kt │ │ │ │ ├── GameEvent.kt │ │ │ │ ├── GameType.kt │ │ │ │ ├── GraveyardCard.kt │ │ │ │ ├── InsertStackResult.kt │ │ │ │ ├── LogType.kt │ │ │ │ ├── Mechanics.kt │ │ │ │ ├── NestedTag.kt │ │ │ │ ├── PowerTag.kt │ │ │ │ ├── Race.kt │ │ │ │ ├── Rarity.kt │ │ │ │ ├── SpellSchool.kt │ │ │ │ ├── Turn.kt │ │ │ │ ├── Zone.kt │ │ │ │ └── ZoneCard.kt │ │ │ ├── parser/ │ │ │ │ ├── BlockTagStack.kt │ │ │ │ ├── DeckCardObserver.kt │ │ │ │ ├── DeckFileObserver.kt │ │ │ │ ├── PowerFileObserver.kt │ │ │ │ ├── PowerParser.kt │ │ │ │ └── PowerTagHandler.kt │ │ │ ├── service/ │ │ │ │ ├── ItemViewTouchListener.kt │ │ │ │ ├── ScaleTouchListener.kt │ │ │ │ └── WindowService.kt │ │ │ └── ui/ │ │ │ ├── chart/ │ │ │ │ ├── GetSummaryChartViewDataUseCase.kt │ │ │ │ ├── PieChartData.kt │ │ │ │ ├── SummaryChartActivity.kt │ │ │ │ └── SummaryChartViewData.kt │ │ │ ├── classbattledetail/ │ │ │ │ ├── ClassBattleDetailActivity.kt │ │ │ │ ├── ClassBattleDetailViewModel.kt │ │ │ │ ├── ClassBattleItem.kt │ │ │ │ └── GetClassBattleItemListUseCase.kt │ │ │ ├── common/ │ │ │ │ ├── CardAdapter.kt │ │ │ │ └── LoadingFragment.kt │ │ │ ├── deck/ │ │ │ │ ├── DeckCodeParserActivity.kt │ │ │ │ └── DeckCodeParserViewModel.kt │ │ │ ├── deckbattledetail/ │ │ │ │ ├── BattleRecordsFragment.kt │ │ │ │ ├── BattleRecordsViewModel.kt │ │ │ │ ├── DeckBattleDetailActivity.kt │ │ │ │ ├── DeckDetailFragment.kt │ │ │ │ ├── DeckDetailViewModel.kt │ │ │ │ ├── DeckFragment.kt │ │ │ │ ├── DeckViewModel.kt │ │ │ │ ├── GetGamesByDeckCodeAndNameUseCase.kt │ │ │ │ ├── SummaryFragment.kt │ │ │ │ └── SummaryViewModel.kt │ │ │ ├── diagnose/ │ │ │ │ └── DiagnoseActivity.kt │ │ │ ├── filter/ │ │ │ │ └── FilterActivity.kt │ │ │ ├── main/ │ │ │ │ ├── CardListFragment.kt │ │ │ │ ├── DeckCardListFragment.kt │ │ │ │ ├── GraveyardFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ ├── OpponentGraveyardFragment.kt │ │ │ │ ├── OpponentHandCardsFragment.kt │ │ │ │ └── UserGraveyardFragment.kt │ │ │ ├── migrate/ │ │ │ │ ├── MigrateDataConvert.kt │ │ │ │ ├── MigrateMainActivity.kt │ │ │ │ ├── SocketClientActivity.kt │ │ │ │ └── SocketServerActivity.kt │ │ │ ├── permissions/ │ │ │ │ ├── PermissionsActivity.kt │ │ │ │ └── PermissionsViewModel.kt │ │ │ ├── records/ │ │ │ │ ├── RecordAdapter.kt │ │ │ │ ├── RecordsActivity.kt │ │ │ │ └── RecordsViewModel.kt │ │ │ ├── settings/ │ │ │ │ ├── SettingsActivity.kt │ │ │ │ └── SettingsViewModel.kt │ │ │ ├── splash/ │ │ │ │ ├── SplashActivity.kt │ │ │ │ └── SplashViewModel.kt │ │ │ ├── summary/ │ │ │ │ ├── BattleRateItem.kt │ │ │ │ ├── BattleRateItemAdapter.kt │ │ │ │ ├── BattleRateListFragment.kt │ │ │ │ ├── BattleRateListViewModel.kt │ │ │ │ ├── DeckBattleRateListViewModel.kt │ │ │ │ ├── GetDeckBattleRateListUseCase.kt │ │ │ │ ├── GetHeroBattleRateListUseCase.kt │ │ │ │ ├── HeroBattleRateListViewModel.kt │ │ │ │ ├── RateByDeckFragment.kt │ │ │ │ ├── RateByHeroFragment.kt │ │ │ │ └── SummaryActivity.kt │ │ │ ├── support/ │ │ │ │ └── SupportActivity.kt │ │ │ ├── sync/ │ │ │ │ ├── SyncCardDataActivity.kt │ │ │ │ └── SyncCardDataViewModel.kt │ │ │ ├── test/ │ │ │ │ ├── CreateRecordActivity.kt │ │ │ │ ├── LocalFileParserActivity.kt │ │ │ │ └── TestActivity.kt │ │ │ ├── theme/ │ │ │ │ └── ThemeActivity.kt │ │ │ ├── writeconfig/ │ │ │ │ └── WriteConfigActivity.kt │ │ │ ├── zonecards/ │ │ │ │ └── ZoneCardsActivity.kt │ │ │ └── zoneevents/ │ │ │ ├── ListModeFragment.kt │ │ │ ├── ZoneEventsActivity.kt │ │ │ └── ZoneEventsViewModel.kt │ │ └── res/ │ │ ├── color/ │ │ │ └── module_game_state.xml │ │ ├── drawable/ │ │ │ ├── module_baseline_arrow_back_white_24dp.xml │ │ │ ├── module_baseline_clear_black_24dp.xml │ │ │ ├── module_baseline_clear_red_500_24dp.xml │ │ │ ├── module_baseline_done_black_24dp.xml │ │ │ ├── module_baseline_done_green_500_24dp.xml │ │ │ ├── module_baseline_done_white_24dp.xml │ │ │ ├── module_baseline_drag_handle_white_24dp.xml │ │ │ ├── module_baseline_keyboard_arrow_right_grey_500_24dp.xml │ │ │ ├── module_baseline_pie_chart_white_24dp.xml │ │ │ ├── module_baseline_play_arrow_white_24dp.xml │ │ │ ├── module_baseline_settings_white_24dp.xml │ │ │ ├── module_baseline_sync_white_24dp.xml │ │ │ └── module_baseline_zoom_out_map_white_24dp.xml │ │ ├── drawable-xxxhdpi/ │ │ │ └── module_bg_splash.xml │ │ ├── layout/ │ │ │ ├── module_activity_class_battle_detail.xml │ │ │ ├── module_activity_create_record.xml │ │ │ ├── module_activity_deck_battle_detail.xml │ │ │ ├── module_activity_deck_code_parser.xml │ │ │ ├── module_activity_diagnose.xml │ │ │ ├── module_activity_filter.xml │ │ │ ├── module_activity_local_file_parser.xml │ │ │ ├── module_activity_main.xml │ │ │ ├── module_activity_migrate_main.xml │ │ │ ├── module_activity_permissions.xml │ │ │ ├── module_activity_records.xml │ │ │ ├── module_activity_settings.xml │ │ │ ├── module_activity_socket_client.xml │ │ │ ├── module_activity_socket_server.xml │ │ │ ├── module_activity_summary.xml │ │ │ ├── module_activity_summary_chart.xml │ │ │ ├── module_activity_support.xml │ │ │ ├── module_activity_sync_card_data.xml │ │ │ ├── module_activity_test.xml │ │ │ ├── module_activity_theme.xml │ │ │ ├── module_activity_write_config.xml │ │ │ ├── module_activity_zone_cards.xml │ │ │ ├── module_activity_zone_events.xml │ │ │ ├── module_dialog_card_preview.xml │ │ │ ├── module_floating_window.xml │ │ │ ├── module_fragment_card_list.xml │ │ │ ├── module_fragment_deck_battle_detail_deck_detail.xml │ │ │ ├── module_fragment_deck_battle_detail_summary.xml │ │ │ ├── module_fragment_graveyard.xml │ │ │ ├── module_fragment_list_mode.xml │ │ │ ├── module_fragment_loading.xml │ │ │ ├── module_fragment_opponent_hand_cards.xml │ │ │ ├── module_header_summary.xml │ │ │ ├── module_item_card.xml │ │ │ ├── module_item_chip_filter.xml │ │ │ ├── module_item_class_battle_detail.xml │ │ │ ├── module_item_footer_with_fab.xml │ │ │ ├── module_item_opponent_hand_card.xml │ │ │ ├── module_item_record.xml │ │ │ ├── module_item_summary_battle.xml │ │ │ ├── module_item_text.xml │ │ │ ├── module_item_zone_card.xml │ │ │ └── module_item_zone_event_list_mode.xml │ │ ├── values/ │ │ │ ├── arrays.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ ├── values-night/ │ │ │ └── themes.xml │ │ └── values-zh-rCN/ │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── ke/ │ └── hs_tracker/ │ └── module/ │ └── ExampleUnitTest.kt ├── settings.gradle ├── shared/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── ke/ │ └── hs/ │ └── shared/ │ └── entity/ │ └── CardType.kt ├── simulator/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── ke/ │ │ └── hs/ │ │ └── simulator/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── ke/ │ │ └── hs/ │ │ └── simulator/ │ │ └── cards/ │ │ └── base/ │ │ ├── HeroCard.kt │ │ ├── ICard.kt │ │ ├── MinionCard.kt │ │ ├── SpellCard.kt │ │ └── WeaponCard.kt │ └── test/ │ └── java/ │ └── com/ │ └── ke/ │ └── hs/ │ └── simulator/ │ └── ExampleUnitTest.kt └── writer/ ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release/ │ ├── output-metadata.json │ └── writer-release.apk └── src/ ├── androidTest/ │ └── java/ │ └── com/ │ └── ke/ │ └── hs/ │ └── writer/ │ └── ExampleInstrumentedTest.kt ├── main/ │ ├── AndroidManifest.xml │ └── res/ │ ├── drawable/ │ │ └── ic_launcher_background.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── values-night/ │ └── themes.xml └── test/ └── java/ └── com/ └── ke/ └── hs/ └── writer/ └── ExampleUnitTest.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx local.properties ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 keluokeda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # hs_tracker ## Android14 use this https://github.com/keluokeda/Hs An automatic Hearthstone tracker for Android #### 炉石传说记牌器,支持Android12、Android13 ![Screenshot_20220423-132616_Hearthstone](https://user-images.githubusercontent.com/16809185/199713461-c0a16e10-e225-4c2a-894d-a1f4dfc51824.jpg) ![image](https://user-images.githubusercontent.com/16809185/201462989-9a826302-b1a7-4674-825c-bfd13711efc9.png) ### QQ群:825215274 ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'com.google.dagger.hilt.android' id 'kotlin-parcelize' } kapt { correctErrorTypes true } android { compileSdk libs.versions.compilesdk.get().toInteger() defaultConfig { applicationId "com.ke.hs_tracker.app" minSdk libs.versions.minsdk.get().toInteger() targetSdk libs.versions.targetsdk.get().toInteger() versionCode 31 versionName "1.3.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildFeatures { viewBinding = true } signingConfigs { myConfig { storeFile file(RELEASE_FILE) storePassword RELEASE_storePassword keyAlias RELEASE_keyAlias keyPassword RELEASE_keyPassword } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { minifyEnabled false zipAlignEnabled true signingConfig signingConfigs.myConfig proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //signingConfig signingConfigs.config } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } namespace 'com.ke.hs_tracker.app' } dependencies { implementation project(path: ':module') implementation(libs.core.ktx) implementation(libs.appcompat) implementation(libs.material) implementation(libs.constraint.layout) implementation(libs.fragment.ktx) implementation(libs.activity) implementation(libs.lifecycle.viewmodel.ktx) implementation(libs.lifecycle.livedata.ktx) implementation(libs.lifecycle.runtime.ktx) implementation(libs.support.v4) implementation(libs.logger) implementation(libs.hilt.android) kapt(libs.hilt.compiler) implementation(libs.ke.mvvm) implementation(libs.moshi) kapt(libs.moshi.codegen) implementation(libs.hilt.android) kapt(libs.hilt.compiler) } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/release/output-metadata.json ================================================ { "version": 3, "artifactType": { "type": "APK", "kind": "Directory" }, "applicationId": "com.ke.hs_tracker.app", "variantName": "release", "elements": [ { "type": "SINGLE", "filters": [], "attributes": [], "versionCode": 31, "versionName": "1.3.1", "outputFile": "app-release.apk" } ], "elementType": "File" } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/ke/hs_tracker/app/App.kt ================================================ package com.ke.hs_tracker.app import com.ke.hs_tracker.module.MainApplication import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class App : MainApplication() ================================================ FILE: app/src/main/java/com/ke/hs_tracker/app/MainActivity.kt ================================================ package com.ke.hs_tracker.app // //import android.os.Bundle //import androidx.appcompat.app.AppCompatActivity //import androidx.documentfile.provider.DocumentFile //import androidx.lifecycle.lifecycleScope //import com.ke.hs_tracker.app.databinding.ActivityMainBinding //import com.ke.hs_tracker.module.findHSDataFilesDir //import com.ke.hs_tracker.module.log //import kotlinx.coroutines.Dispatchers //import kotlinx.coroutines.launch //import kotlinx.coroutines.withContext // //class MainActivity : AppCompatActivity() { // // // private lateinit var powerLogFile: DocumentFile // // private lateinit var binding: ActivityMainBinding // //// private lateinit var inputStream: InputStream // // private var oldLogSize = 0L // // override fun onCreate(savedInstanceState: Bundle?) { // super.onCreate(savedInstanceState) // binding = ActivityMainBinding.inflate(layoutInflater) // setContentView(binding.root) // // // // binding.init.setOnClickListener { // lifecycleScope.launch { // // oldLogSize = 0 // val logsDir = findHSDataFilesDir("Logs") // // val file = logsDir?.findFile("Power.log") // if (file != null) { // "初始化成功 $file".log() // powerLogFile = file // } else { // "初始化失败".log() // } // // // } // } // // binding.delete.setOnClickListener { // try { // "删除本地日志结果 ${deleteFile("power.log")}".log() // // val result = powerLogFile.delete() // "删除文件结果 $result".log() // } catch (e: Exception) { //// binding.content.text = e.message // "删除文件失败".log() // e.printStackTrace() // } // } // // binding.load.setOnClickListener { // // lifecycleScope.launch { // try { // // // contentResolver.openInputStream(powerLogFile.uri)!!.reader() // .apply { // if (oldLogSize > 0) { // val skip = skip(oldLogSize) // "跳过的字节数量 $skip".log() // } // binding.content.text = readText().also { // oldLogSize += it.length // writeToLocal(it) // } // // close() // } // // } catch (e: Exception) { // binding.content.text = e.message // } // } // // // } // } // // private suspend fun writeToLocal(content: String) { // withContext(Dispatchers.IO) { // openFileOutput("power.log", MODE_APPEND) // .writer().apply { // append(content) // flush() // close() // } // } // } // // //// private suspend fun getPowerLogFileSize(): Long { //// return withContext(Dispatchers.IO) { //// try { //// contentResolver.query(powerLogFile.uri, arrayOf(OpenableColumns.SIZE), null, null) //// ?.apply { //// moveToFirst() //// return@withContext getLong(0).also { //// close() //// } //// //// } //// } catch (e: Exception) { //// e.printStackTrace() //// return@withContext 0 //// } //// //// //// //// 0 //// } //// } //} ================================================ FILE: build.gradle ================================================ plugins { id 'com.android.application' version '8.0.2' apply false id 'com.android.library' version '8.0.2' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false id 'com.google.dagger.hilt.android' version '2.45' apply false } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: core/.gitignore ================================================ /build ================================================ FILE: core/build.gradle ================================================ plugins { id 'java-library' id 'org.jetbrains.kotlin.jvm' id 'kotlin-kapt' } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { implementation(libs.retrofit) implementation(libs.retrofit.converter.moshi) implementation(libs.moshi) kapt(libs.moshi.codegen) implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' // implementation("com.squareup.moshi:moshi-kotlin:1.13.0") } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/api/HearthStoneJsonApi.kt ================================================ package com.ke.hs_tracker.core.api import com.ke.hs_tracker.core.entity.Card import retrofit2.http.GET import retrofit2.http.Path interface HearthStoneJsonApi { /** * 获取卡牌数据 */ @GET("v1/{versionCode}/{region}/cards.json") suspend fun getCardJsonList( @Path("versionCode") versionCode: String, @Path("region") region: String, ): List companion object { const val BASE_URL = "https://api.hearthstonejson.com/" } } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/BlockType.kt ================================================ package com.ke.hs_tracker.core.entity enum class BlockType { /** * 攻击 */ Attack, /** * 死亡 */ Deaths, /** * 触发 */ Trigger, /** * 打出一张卡牌 */ Play, /** * 卡牌生效 */ Power, /** * 交易 */ Trade } internal fun String.toBlockType(fallback: BlockType = BlockType.Trigger): BlockType { BlockType.values().forEach { if (it.name.equals(this, true)) { return it } } return fallback } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/Card.kt ================================================ package com.ke.hs_tracker.core.entity import com.squareup.moshi.FromJson import com.squareup.moshi.JsonClass import com.squareup.moshi.ToJson @JsonClass(generateAdapter = true) data class Card( val name: String, val cost: Int = 0, val id: String, val dbfId: Int, val text: String = "", //属于哪个版本 例如 TGT val set: String, val type: CardType = CardType.None, val cardClass: CardClass, val classes: List = emptyList(), val flavor: String, val attach: Int = 0, val health: Int = 0 ) enum class CardType { /** * 英雄 */ Hero, /** * 英雄技能 */ HeroPower, /** *衍生牌 */ Enchantment, /** * 法术 */ Spell, /** * 随从 */ Minion, /** * 武器 */ Weapon, None } class CardTypeAdapter { @FromJson fun fromJson(value: String): CardType { return CardType.values() .find { it.name.equals(value.replace("_", ""), true) } ?: CardType.None } @ToJson fun toJson(cardType: CardType) = cardType.name.uppercase() } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/CardClass.kt ================================================ package com.ke.hs_tracker.core.entity enum class CardClass { /** * 法师 */ Mage, /** * 术士 */ Warlock, /** * 牧师 */ Priest, /** * 德鲁伊 */ Druid, Neutral } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/CurrentDeck.kt ================================================ package com.ke.hs_tracker.core.entity data class CurrentDeck( val name: String, val code: String ) ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/Entity.kt ================================================ package com.ke.hs_tracker.core.entity import com.ke.hs_tracker.core.parser.PowerParserImpl //有三种形式 //1,Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1] //2,Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1] //3,Entity=失落的裤子#5629 data class Entity( val entityName: String, val gameCardType: GameCardType? = null, val id: Int = -1, val zone: Zone = Zone.Play, val zonePosition: Int = -1, val cardId: String? = null, val player: Int = -1 ) { val entityType: EntityType get() = when { gameCardType == null && id == -1 && zone == Zone.Play && zonePosition == -1 && cardId == null && player == -1 -> EntityType.Name gameCardType == GameCardType.Invalid -> EntityType.Invalid else -> EntityType.Clear } companion object { internal fun createFromContent(content: String): Entity? { if (content == "0") { return null } var matchResult = PowerParserImpl.FULL_ENTITY_CONTENT1_PATTERN.matchEntire(content) if (matchResult != null) { return Entity( matchResult.groupValues[1], matchResult.groupValues[2].toCardType(GameCardType.Invalid), matchResult.groupValues[3].toIntOrNull() ?: 0, matchResult.groupValues[4].toZone(), matchResult.groupValues[5].toIntOrNull() ?: 0, matchResult.groupValues[6].ifBlank { null }, matchResult.groupValues[7].toIntOrNull() ?: 0 ) } matchResult = PowerParserImpl.FULL_ENTITY_CONTENT2_PATTERN.matchEntire(content) ?: return Entity(content) return Entity( matchResult.groupValues[1], GameCardType.Invalid, matchResult.groupValues[2].toIntOrNull() ?: 0, matchResult.groupValues[3].toZone(), matchResult.groupValues[4].toIntOrNull() ?: 0, matchResult.groupValues[5].ifBlank { null }, matchResult.groupValues[6].toIntOrNull() ?: 0 ) } } } enum class EntityType { //Entity=失落的裤子#5629 Name, //Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1] Invalid, //Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1] Clear } data class GameEntity( val gameCardType: GameCardType, val entityId: Int ) /** * 玩家 */ data class Player( val entityId: Int, val playerId: Int, val controller: Int, val gameCardType: GameCardType, val heroEntity: Int, /** * 手牌上限 */ val maxHandSize: Int, /** * 起始手牌 */ val startHandSize: Int, val teamId: Int, /** * 费用上限 一般为10 */ val maxResources: Int ) { companion object { internal fun fromMap(map: Map): Player { return Player( entityId = map["entityid"]?.toIntOrNull() ?: 0, playerId = map["playerid"]?.toIntOrNull() ?: 0, controller = map["controller"]?.toIntOrNull() ?: 0, gameCardType = (map["cardtype"] ?: "").toCardType(), heroEntity = map["heroentity"]?.toIntOrNull() ?: 0, maxHandSize = map["maxhandsize"]?.toIntOrNull() ?: 0, startHandSize = map["starthandsize"]?.toIntOrNull() ?: 0, teamId = map["teamid"]?.toIntOrNull() ?: 0, maxResources = map["maxresources"]?.toIntOrNull() ?: 0 ) } } } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/GameCardType.kt ================================================ package com.ke.hs_tracker.core.entity /** * 卡牌类型 */ enum class GameCardType { Game, /** * 玩家 */ Player, /** * 英雄 */ Hero, /** * 英雄技能 */ HeroPower, /** * 牌库中的牌的状态 */ Invalid, /** * 随从身上的buff或战场上的buff(例如下一张法强怪法力值减少1) */ Enchantment, /** * 法术 */ Spell, /** * 随从 */ Minion } /** * 字符串转 CardType类型 */ internal fun String.toCardType(fallback: GameCardType = GameCardType.Game): GameCardType { return GameCardType.values().find { it.name.equals(this.replace("_", ""), true) } ?: fallback } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/InsertStackResult.kt ================================================ package com.ke.hs_tracker.core.entity sealed interface InsertStackResult { /** * 插入成功 */ object Success : InsertStackResult /** * 不能插入,例如不在Block内的TAG_CHANGE */ object CanNotInsert : InsertStackResult /** * 结束了 * @param powerTag tag * @param handled 是否处理了本次log日志,例如 FULL_ENTITY - Updating 跟着一个 FULL_ENTITY - Updating的情况,handled就是true,表示已经处理了,不需要在进行处理;如果是FULL_ENTITY - Updating * 跟着一个 TAG_CHANGE,就表示没有处理,需要调用这个方法的自行处理log数据 */ data class Over(val powerTag: PowerTag, val handled: Boolean) : InsertStackResult } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/LogType.kt ================================================ package com.ke.hs_tracker.core.entity enum class LogType(val replace: String) { PowerTaskList("PowerTaskList.DebugPrintPower() -"), GameState("GameState.DebugPrintGame() -") } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/Mechanics.kt ================================================ package com.ke.hs_tracker.core.entity /** * 类型 */ enum class Mechanics { /** * 过载 */ Overload, /** * 战吼 */ Battlecry, /** * 嘲讽 */ Taunt, /** * 突袭 */ Rush, /** * 潜行 */ Stealth, /** * 圣盾 */ DivineShield, /** * 法强 */ SpellPower, /** * 亡语 */ Deathrattle, /** * 荣誉击杀 */ hHonorableKill, /** * 法力迸发 */ SpellBurst, /** * 腐蚀 */ Corrupt, /** * 暴怒 */ Frenzy, /** * 发现 */ Discover, /** * 冻结 */ Freeze, /** * 连击 */ Combo, /** * 无法攻击 */ CantAttack, /** * 触发 */ TriggerVisual, /** * 奥秘 */ Secret, /** * 交易 */ Tradeable, /** * 激励 */ Inspire, /** * 沉默 */ Silence, /** * 风怒 */ Windfury, /** * 回响 */ Echo } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/NestedTag.kt ================================================ package com.ke.hs_tracker.core.entity internal sealed interface NestedTag { object CreateGame : NestedTag data class GameEntity(val id: Int) : NestedTag data class Tag(val key: String, val value: String) : NestedTag { fun toPair(): Pair = key to value } data class Player(val entityId: Int, val playerId: Int) : NestedTag //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] CardID= //FULL_ENTITY - Updating [entityName=全副武装! id=65 zone=PLAY zonePos=0 cardId=HERO_01bp player=1] CardID=HERO_01bp data class FullEntity( val entity: Entity ) : NestedTag data class Block( val blockType: BlockType, val entity: Entity, val target: Entity? ) : NestedTag data class TagChange( val entity: Entity, val tag: String, val value: String ) : NestedTag { fun convert(): PowerTag.PowerTaskList.TagChange { return PowerTag.PowerTaskList.TagChange(entity, tag, value) } } data class ShowEntity( val entity: Entity, val cardId: String ) : NestedTag object BlockEnd : NestedTag } //internal fun NestedTag.FullEntity.toUpdating(): PowerTag.PowerTaskList.FullEntity.Updating { // return PowerTag.PowerTaskList.FullEntity.Updating( // entityName, cardType, id, zone, zonePosition, cardId, player // ) //} ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/PowerTag.kt ================================================ package com.ke.hs_tracker.core.entity sealed interface PowerTag { sealed interface PowerTaskList : PowerTag { /** * 创建游戏 */ //D 19:55:18.1257030 GameState.DebugPrintPower() - CREATE_GAME //D 19:55:18.1257030 GameState.DebugPrintPower() - GameEntity EntityID=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=GAME //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=937 value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=GAME_SEED value=1950487951 //D 19:55:18.1257030 GameState.DebugPrintPower() - Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280] //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CONTROLLER value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=PLAYER //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=PLAYER_ID value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=HERO_ENTITY value=64 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXHANDSIZE value=10 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=STARTHANDSIZE value=4 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=TEAM_ID value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXRESOURCES value=10 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=AVRANK value=336 //D 19:55:18.1257030 GameState.DebugPrintPower() - Player EntityID=3 PlayerID=2 GameAccountId=[hi=144115211015832391 lo=44511141] //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CONTROLLER value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=PLAYER //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=PLAYER_ID value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=HERO_ENTITY value=66 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXHANDSIZE value=10 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=STARTHANDSIZE value=4 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=TEAM_ID value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=3 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXRESOURCES value=10 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=AVRANK value=338 data class CreateGame( val gameEntity: GameEntity, val player1: Player, val player2: Player ) : PowerTaskList data class TagChange( val entity: Entity, val tag: String, val value: String, ) : PowerTaskList { /** * 是否是游戏完成的标志 */ val isGameComplete: Boolean = entity.entityName == "GameEntity" && tag.equals("state", true) && value.equals( "COMPLETE", true ) } data class FullEntity( val entity: Entity, val payloads: MutableMap = mutableMapOf() ) : PowerTaskList { fun append(value: Pair) { payloads[value.first] = value.second } } data class ShowEntity( val entity: Entity, val cardId: String, val payloads: MutableMap = mutableMapOf() ) : PowerTaskList { fun append(value: Pair) { payloads[value.first] = value.second } } data class Block( val type: BlockType, val entity: Entity, val target: Entity?, val list: List ) : PowerTaskList } sealed interface GameState : PowerTag { data class BuildNumber(val number: String) : GameState data class GameType(val type: String) : GameState data class FormatType(val type: String) : GameState data class ScenarioID(val id: String) : GameState data class PlayerMapping(val id: Int, val name: String) : GameState { /** * 是否是先手 */ val first: Boolean = id == 1 /** * 是否是当前用户 */ val isUser: Boolean = name != "UNKNOWN HUMAN PLAYER" } } } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/entity/Zone.kt ================================================ package com.ke.hs_tracker.core.entity enum class Zone { /** * 战场 */ Play, /** * 牌库 */ Deck, /** * 发现的牌的位置 */ SetAside, /** * 墓地 打出的法术牌和死亡的随从会进入 */ Graveyard, /** * 手牌 */ Hand } internal fun String.toZone(fallback: Zone = Zone.Deck): Zone { return Zone.values().firstOrNull { it.name.equals(this, true) } ?: fallback } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/extensions.kt ================================================ package com.ke.hs_tracker.core import com.ke.hs_tracker.core.parser.PowerParserImpl import java.util.* fun String.removeTime(): Triple { val content = substring(PowerParserImpl.TIME_PREFIX_SIZE) val start = substring(0, 1) val hms = substring(2, 10).split(":") val calendar = Calendar.getInstance() calendar.set( Calendar.HOUR_OF_DAY, hms[0].toInt() ) calendar.set( Calendar.MINUTE, hms[1].toInt() ) calendar.set( Calendar.SECOND, hms[2].toInt() ) return Triple( start, calendar.time, content ) } fun main(){ val text = "I 22:23:35.6401730 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA" println(text.removeTime().toString()) } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/parser/BlockTagStack.kt ================================================ package com.ke.hs_tracker.core.parser import com.ke.hs_tracker.core.entity.* import java.lang.Exception import java.util.* import kotlin.text.lowercase import kotlin.text.replace import kotlin.text.toInt interface BlockTagStack { /** * 插入一条日志 */ fun insert(line: String): InsertStackResult } internal class BlockTagStackImpl : BlockTagStack { private val nestedTagList = LinkedList() override fun insert(line: String): InsertStackResult { //CREATE_GAME var matchResult = PowerParserImpl.CREATE_GAME_PATTERN.matchEntire(line) if (matchResult != null) { //游戏开始 nestedTagList.clear() nestedTagList.add(NestedTag.CreateGame) return InsertStackResult.Success } //GameEntity EntityID=1 matchResult = PowerParserImpl.GAME_ENTITY_PATTERN.matchEntire(line) if (matchResult != null) { val entityId = matchResult.groupValues[1].toInt() nestedTagList.add(NestedTag.GameEntity(entityId)) return InsertStackResult.Success } //Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280] matchResult = PowerParserImpl.PLAYER_ENTITY_PATTERN.matchEntire(line) if (matchResult != null) { val entityId = matchResult.groupValues[1].toInt() val playerId = matchResult.groupValues[2].toInt() nestedTagList.add(NestedTag.Player(entityId, playerId)) return InsertStackResult.Success } //tag=CARDTYPE value=GAME matchResult = PowerParserImpl.TAG_PATTERN.matchEntire(line) if (matchResult != null) { val key = matchResult.groupValues[1] val value = matchResult.groupValues[2] nestedTagList.add(NestedTag.Tag(key, value)) return InsertStackResult.Success } //BLOCK_START // BlockType=TRIGGER // Entity=GameEntity // EffectCardId=System.Collections.Generic.List`1[System.String] // EffectIndex=-1 // Target=0 // SubOption=-1 // TriggerKeyword=TAG_NOT_SET matchResult = PowerParserImpl.BLOCK_START_PATTERN.matchEntire(line) if (matchResult != null) { val blockType = matchResult.groupValues[1].toBlockType() val entity = Entity.createFromContent(matchResult.groupValues[2])!! val target = Entity.createFromContent(matchResult.groupValues[5]) val block = NestedTag.Block(blockType, entity, target) nestedTagList.add(block) return InsertStackResult.Success } matchResult = PowerParserImpl.BLOCK_END_PATTERN.matchEntire(line) if (matchResult != null) { //块结束了 nestedTagList.add(NestedTag.BlockEnd) // val blockStartList = nestedTagList.filterIsInstance() val blockCount = nestedTagList.count { it is NestedTag.Block } val blockEndCount = nestedTagList.count { it is NestedTag.BlockEnd } if (blockCount == blockEndCount) { return InsertStackResult.Over(flushBlock(nestedTagList), true) } else { return InsertStackResult.Success } } //TAG_CHANGE Entity=阿克萌德#51240 tag=CURRENT_PLAYER value=1 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=1 matchResult = PowerParserImpl.TAG_CHANGE_PATTERN.matchEntire(line) if (matchResult != null) { return when (val first = nestedTagList.firstOrNull()) { is NestedTag.FullEntity -> { val powerTag = flushFullEntityWhenFirst() //处理堆栈 InsertStackResult.Over(powerTag, false) } is NestedTag.ShowEntity -> { val showEntity = PowerTag.PowerTaskList.ShowEntity( first.entity, first.cardId ) nestedTagList.forEach { if (it is NestedTag.TagChange) { showEntity.payloads[it.tag] = it.value } } nestedTagList.clear() InsertStackResult.Over(showEntity, false) } is NestedTag.Block -> { val tagChange = NestedTag.TagChange( Entity.createFromContent(matchResult.groupValues[1])!!, matchResult.groupValues[2], matchResult.groupValues[3], ) nestedTagList.add(tagChange) InsertStackResult.Success } else -> { InsertStackResult.CanNotInsert } } } //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1] CardID= matchResult = PowerParserImpl.FULL_ENTITY_PATTERN.matchEntire(line) if (matchResult != null) { val first = nestedTagList.firstOrNull() if (first is NestedTag.CreateGame) { //create game 接 full entity val createGame = createCreateGameTag() val content = matchResult.groupValues[1] insertFullEntity(content) return InsertStackResult.Over(createGame, true) } else if (first is NestedTag.FullEntity) { //连续两个full entity val result = flushFullEntityWhenFirst() insertFullEntity(matchResult.groupValues[1]) return InsertStackResult.Over(result, true) } insertFullEntity(matchResult.groupValues[1]) return InsertStackResult.Success } //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=85 zone=SETASIDE zonePos=0 cardId= player=2] CardID=SCH_231e matchResult = PowerParserImpl.SHOW_ENTITY.matchEntire(line) if (matchResult != null) { val first = nestedTagList.firstOrNull() val fullEntity = if (first is NestedTag.FullEntity) { flushFullEntityWhenFirst() } else null val entity = Entity.createFromContent(matchResult.groupValues[1])!! val cardId = matchResult.groupValues[2] nestedTagList.add(NestedTag.ShowEntity(entity, cardId)) return if (fullEntity == null) { InsertStackResult.Success } else { InsertStackResult.Over(fullEntity, true) } } return InsertStackResult.CanNotInsert } /** * 如果栈中的第一个是FullEntity,就开始处理 */ private fun flushFullEntityWhenFirst(): PowerTag.PowerTaskList.FullEntity { val map = mutableMapOf() val first = nestedTagList.removeFirst() as NestedTag.FullEntity nestedTagList.map { it as NestedTag.Tag }.forEach { map[it.key] = it.value } //清空栈 nestedTagList.clear() return PowerTag.PowerTaskList.FullEntity( first.entity, map ) // return when { // isInsertCardToDeck(first) -> { // //插入一张卡牌到牌库 // flushFullEntityInsertCardToDeck() // } // isInsertHeroToPlay(first) -> { // //放置英雄牌到战场 // flushFullEntityInsertHeroToPlay() // } // isInsertHeroPowerToPlay(first) -> { // //放置英雄技能到战场 // flushFullEntityInsertHeroPowerToPlay() // } // else -> throw RuntimeException("无法处理的 full entity $first") // } } // /** // * 是否是置入英雄技能到战场 // */ // private fun isInsertHeroPowerToPlay(fullEntity: NestedTag.FullEntity): Boolean { // if (fullEntity.entity.zone != Zone.Play) { // return false // } // // nestedTagList.forEach { // if (it is NestedTag.Tag && "cardType".equals( // it.key, // true // ) && GameCardType.HeroPower.name.equals( // it.value.replace("_", ""), // true // ) // ) { // return true // } // } // // // return false // } // private fun flushFullEntityInsertHeroPowerToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay { // var last = nestedTagList.removeLastOrNull() // //移除第一个 // val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity // val map = mutableMapOf() // while (last != null) { // when (last) { // is NestedTag.Tag -> { // map[last.key] = last.value // } // else -> { // throw RuntimeException("last的类型必须是Tag 但现在是 $last") // } // } // last = nestedTagList.removeLastOrNull() // } // return PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay.createFromEntityAndMap( // entity, // map // ) // } // // private fun flushFullEntityInsertHeroToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay { // var last = nestedTagList.removeLastOrNull() // //移除第一个 // val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity // val map = mutableMapOf() // while (last != null) { // when (last) { // is NestedTag.Tag -> { // map[last.key] = last.value // } // else -> { // throw RuntimeException("last的类型必须是Tag 但现在是 $last") // } // } // last = nestedTagList.removeLastOrNull() // } // return PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay.createFromEntityAndMap( // entity, // map // ) // } // /** // * 是否是置入英雄卡到战场 // */ // private fun isInsertHeroToPlay(fullEntity: NestedTag.FullEntity): Boolean { // if (fullEntity.entity.zone != Zone.Play) { // return false // } // // nestedTagList.forEach { // if (it is NestedTag.Tag && "cardType".equals( // it.key, // true // ) && GameCardType.Hero.name.equals( // it.value, // true // ) // ) { // return true // } // } // // // return false // } // /** // * 是否是置入英雄卡到战场 // */ // private fun isInsertCardToDeck(fullEntity: NestedTag.FullEntity): Boolean { // // return fullEntity.entity.zone == Zone.Deck && fullEntity.entity.gameCardType == GameCardType.Invalid // } // private fun flushFullEntityInsertCardToDeck(): PowerTag.PowerTaskList.FullEntity.InsertToDeck { // var last = nestedTagList.removeLastOrNull() // //移除第一个 // val fullEntity = nestedTagList.removeFirst() as NestedTag.FullEntity // val map = mutableMapOf() // while (last != null) { // when (last) { // is NestedTag.Tag -> { // map[last.key] = last.value // } // else -> { // throw RuntimeException("last的类型必须是Tag 但现在是 $last") // } // } // last = nestedTagList.removeLastOrNull() // } // // if (map.size != 3) throw RuntimeException("在插入卡牌到牌库的情况下,tag数量必须是3个") // // return PowerTag.PowerTaskList.FullEntity.InsertToDeck.createFromEntityAndMap( // fullEntity.entity, // map // ) // // } private fun insertFullEntity(content: String) { val fullEntity = createFullEntityByContent(content) nestedTagList.add(fullEntity) } /** * 根据字符串创建FullEntity */ //[entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1] //[entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1] private fun createFullEntityByContent(content: String): NestedTag.FullEntity { val entity: Entity = Entity.createFromContent(content)!! return NestedTag.FullEntity(entity) } private fun createCreateGameTag(): PowerTag.PowerTaskList.CreateGame { val first = nestedTagList.removeFirstOrNull() if (first == NestedTag.CreateGame) { val keyValueMap = mutableMapOf() var last = nestedTagList.removeLastOrNull() var player1: Player? = null var player2: Player? = null while (last != null) { when (last) { is NestedTag.GameEntity -> { val game = GameEntity( GameCardType.Game, last.id ) return PowerTag.PowerTaskList.CreateGame( game, player1!!, player2!! ) } is NestedTag.Player -> { keyValueMap["playerid"] = last.playerId.toString() keyValueMap["entityid"] = last.entityId.toString() if (player2 == null) { player2 = Player.fromMap(keyValueMap) } else { player1 = Player.fromMap(keyValueMap) } keyValueMap.clear() } is NestedTag.Tag -> { keyValueMap[last.key.replace("_", "").lowercase()] = last.value } else -> throw IllegalArgumentException("非法状态错误 $last") } last = nestedTagList.removeLastOrNull() } } else { throw IllegalArgumentException("第一个必须是 CreateGame,但现在是 $first") } throw RuntimeException("无法创建CreateGame") } private fun flushBlock( source: MutableList, ): PowerTag.PowerTaskList.Block { //有可能出现多级嵌套 val first = source.removeFirst() as? NestedTag.Block if (first == null) { throw RuntimeException("列表的第一个应该是Block,但现在是不是") } source.removeLast() val payloads = mutableListOf() val tempList = mutableListOf() source.forEachIndexed { index, nestedTag -> when (nestedTag) { is NestedTag.Block -> { // if (tempList.isEmpty()) { tempList.add(nestedTag) // } else { // val blockStartCount = tempList.count { // it is NestedTag.Block // } // if (blockStartCount != 1) { // throw IllegalArgumentException("错误的block数量 $blockStartCount") // } // // val pairedBlockEndIndex = findPairBlockEndIndex(source, index) // if (pairedBlockEndIndex == -1) { // throw IllegalArgumentException("找不到配对的结束标识") // } // val innerBlockList = source.subList(index, pairedBlockEndIndex) // tempList.add(flushBlock(innerBlockList)) // } } is NestedTag.FullEntity -> { if (tempList.isNotEmpty()) { tempList.add(nestedTag) } else { payloads.add(PowerTag.PowerTaskList.FullEntity(nestedTag.entity)) } } is NestedTag.ShowEntity -> { if (tempList.isNotEmpty()) { tempList.add(nestedTag) } else { payloads.add( PowerTag.PowerTaskList.ShowEntity( nestedTag.entity, nestedTag.cardId ) ) } } is NestedTag.Tag -> { if (tempList.isNotEmpty()) { tempList.add(nestedTag) } else { val last = payloads.last() if (last is PowerTag.PowerTaskList.FullEntity) { last.append(nestedTag.toPair()) } else if (last is PowerTag.PowerTaskList.ShowEntity) { last.append(nestedTag.toPair()) } } } is NestedTag.TagChange -> { if (tempList.isNotEmpty()) { tempList.add(nestedTag) } else { payloads.add(nestedTag.convert()) } } NestedTag.BlockEnd -> { tempList.add(NestedTag.BlockEnd) val blockStartCount = tempList.count { it is NestedTag.Block } val blockEndCount = tempList.count { it is NestedTag.BlockEnd } if (blockStartCount == blockEndCount) { payloads.add(flushBlock(tempList)) tempList.clear() } } else -> { throw IllegalArgumentException("非法的数据 $nestedTag") } } } source.clear() return PowerTag.PowerTaskList.Block( first.blockType, first.entity, first.target, payloads ) } } private fun findPairBlockEndIndex(source: List, start: Int): Int { var blockStartCount = 0 source.subList(start, source.size).forEachIndexed { index, it -> if (it is NestedTag.Block) { blockStartCount++ } else if (it is NestedTag.BlockEnd) { if (blockStartCount == 0) { return index } blockStartCount-- } } return -1 } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/parser/DeckFileObserver.kt ================================================ package com.ke.hs_tracker.core.parser import com.ke.hs_tracker.core.entity.CurrentDeck import com.ke.hs_tracker.core.removeTime import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.io.InputStream /** * deck文件观察者 */ class DeckFileObserver( private val interval: Long = 2000, private val deckFileInputStreamProvider: suspend () -> InputStream?, ) { private var oldLogSize = 0L fun reset(){ oldLogSize = 0 } suspend fun start(): Flow = flow { while (true) { deckFileInputStreamProvider()?.reader()?.apply { if (oldLogSize > 0) { skip(oldLogSize) } val text = readText() oldLogSize += text.length val lines = text.lines() .filter { it.isNotEmpty() } listToDeck(lines)?.apply { emit(this) } close() } delay(interval) } } private fun listToDeck(list: List): CurrentDeck? { if (list.isEmpty()) { return null } val contentList = list.map { it.removeTime().third } val name = contentList.findLast { it.startsWith("###", true) } ?: return null val target = contentList.subList(contentList.indexOf(name), contentList.size).toMutableList() target.removeFirst() target.removeFirst() val code = target.removeFirst() return CurrentDeck( name.replace("### ", ""), code ) } } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/parser/PowerFileObserver.kt ================================================ package com.ke.hs_tracker.core.parser import com.ke.hs_tracker.core.entity.CurrentDeck import com.ke.hs_tracker.core.entity.PowerTag import com.ke.hs_tracker.core.removeTime import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.io.InputStream /** * deck文件观察者 */ class PowerFileObserver( private val interval: Long = 2000, private val fileInputStreamProvider: suspend () -> InputStream?, ) { private var oldLogSize = 0L fun reset(){ oldLogSize = 0 } suspend fun start(): Flow> = flow { while (true) { fileInputStreamProvider()?.reader()?.apply { if (oldLogSize > 0) { skip(oldLogSize) } val text = readText() oldLogSize += text.length val lines = text.lines() .filter { it.isNotEmpty() } emit(lines) close() } delay(interval) } } } ================================================ FILE: core/src/main/java/com/ke/hs_tracker/core/parser/PowerParser.kt ================================================ package com.ke.hs_tracker.core.parser import com.ke.hs_tracker.core.entity.* import com.ke.hs_tracker.core.entity.toCardType import com.ke.hs_tracker.core.removeTime /** * 日志解析 */ interface PowerParser { /** * 解析一行日志 */ fun parse(content: String) /** * 解析结果监听 */ var powerTagListener: ((PowerTag) -> Unit)? } class PowerParserImpl : PowerParser { private val blockTagStack: BlockTagStack = BlockTagStackImpl() override var powerTagListener: ((PowerTag) -> Unit)? = null override fun parse(content: String) { val pair = checkTypeAndReturnContent(content) ?: return if (pair.first == LogType.PowerTaskList) { handlePowerTaskListLog(pair.second) } else { handleGameStateLog(pair.second) } } /** * 检查日志类型并返回去掉时间和日期前缀的内容 */ private fun checkTypeAndReturnContent(content: String): Pair? { if (content.length < TIME_PREFIX_SIZE) { return null } val noTimeContent = content.substring(TIME_PREFIX_SIZE) if (noTimeContent.startsWith(LogType.GameState.replace)) { return LogType.GameState to noTimeContent.replace(LogType.GameState.replace, "").trim() } else if (noTimeContent.startsWith(LogType.PowerTaskList.replace)) { return LogType.PowerTaskList to noTimeContent.replace(LogType.PowerTaskList.replace, "") .trim() } return null } private fun handlePowerTaskListLog(line: String) { val result = blockTagStack.insert(line) when (result) { InsertStackResult.CanNotInsert -> { handleUnSupportNestedTag(line) } is InsertStackResult.Over -> { powerTagListener?.invoke(result.powerTag) if (!result.handled) { //需要自己处理 handleUnSupportNestedTag(line) } } InsertStackResult.Success -> { } } } private fun handleUnSupportNestedTag(line: String) { var matchResult = TAG_CHANGE_PATTERN.matchEntire(line) if (matchResult != null) { handleTagChangeLine( matchResult.groupValues[1], matchResult.groupValues[2], matchResult.groupValues[3] ) } } private fun handleTagChangeLine(content: String, tag: String, value: String) { val entity = Entity.createFromContent(content)!! powerTagListener?.invoke(PowerTag.PowerTaskList.TagChange(entity, tag, value)) } private fun handleGameStateLog(content: String) { BUILD_NUMBER_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.BuildNumber( groupValues[1] ) powerTagListener?.invoke(tag) return } GAME_TYPE_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.GameType( groupValues[1] ) powerTagListener?.invoke(tag) return } FORMAT_TYPE_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.FormatType( groupValues[1] ) powerTagListener?.invoke(tag) return } SCENARIO_ID_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.ScenarioID( groupValues[1] ) powerTagListener?.invoke(tag) return } PLAYER_MAPPING_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.PlayerMapping( groupValues[1].toInt(), groupValues[2] ) powerTagListener?.invoke(tag) return } } companion object { const val TIME_PREFIX_SIZE = 19 //CREATE_GAME internal val CREATE_GAME_PATTERN = Regex("CREATE_GAME") //BuildNumber=127581 internal val BUILD_NUMBER_PATTERN = Regex("BuildNumber=(.*)") //GameType=GT_CASUAL internal val GAME_TYPE_PATTERN = Regex("GameType=(.*)") //FormatType=FT_WILD internal val FORMAT_TYPE_PATTERN = Regex("FormatType=(.*)") //ScenarioID=2 internal val SCENARIO_ID_PATTERN = Regex("ScenarioID=(.*)") //PlayerID=2, PlayerName=阿克萌德#51240 internal val PLAYER_MAPPING_PATTERN = Regex("PlayerID=(.*), PlayerName=(.*)") // tag=CARDTYPE value=GAME internal val TAG_PATTERN = Regex("tag=(.*) value=(.*)") //TAG_CHANGE Entity=GameEntity tag=STATE value=RUNNING internal val TAG_CHANGE_PATTERN = Regex("TAG_CHANGE Entity=(.*) tag=(.*) value=(.*)") //GameEntity EntityID=1 internal val GAME_ENTITY_PATTERN = Regex("GameEntity EntityID=(.*)") //FULL_ENTITY - Updating [entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1] CardID=HERO_01 internal val FULL_ENTITY_PATTERN = Regex("FULL_ENTITY - Updating (.*) CardID=(.*)") //[entityName=UNKNOWN ENTITY [cardType=INVALID] id=83 zone=DECK zonePos=0 cardId= player=2] val FULL_ENTITY_CONTENT1_PATTERN = Regex("\\[entityName=(.*) \\[cardType=(.*)] id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]") val FULL_ENTITY_CONTENT2_PATTERN = Regex("\\[entityName=(.*) id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]") val SHOW_ENTITY = Regex("SHOW_ENTITY - Updating Entity=(.*) CardID=(.*)") //Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280] internal val PLAYER_ENTITY_PATTERN = Regex("Player EntityID=(.*) PlayerID=(.*) GameAccountId=(.*)") //BLOCK_START BlockType=ATTACK Entity=[entityName=瓦丝琪女士 id=87 zone=PLAY zonePos=1 cardId=BT_109 player=2] // EffectCardId=System.Collections.Generic.List`1[System.String] // EffectIndex=0 Target=[entityName=驯化的雷象 id=78 zone=PLAY zonePos=1 cardId=SCH_714 player=1] SubOption=-1 internal val BLOCK_START_PATTERN = Regex("BLOCK_START BlockType=(.*) Entity=(.*) EffectCardId=(.*) EffectIndex=(.*) Target=(.*) SubOption=(.*)") internal val BLOCK_START_CONTINUATION_PATTERN = Regex("(.*) TriggerKeyword=(.*)") //BLOCK_END internal val BLOCK_END_PATTERN = Regex("BLOCK_END") } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue Jan 18 13:48:58 CST 2022 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official #android.jetifier.ignorelist=moshi-1.13.0 RELEASE_FILE=../123456 RELEASE_keyAlias=key0 RELEASE_storePassword=123456 RELEASE_keyPassword=123456 android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: libs.versions.toml ================================================ [versions] compilesdk = "33" minsdk = "24" targetsdk = "33" retrofit = "2.9.0" constraintlayout = "2.1.2" material = "1.6.0" moshi = "1.14.0" ktx = "1.10.0" hilt = "2.45" lifecycle = "2.4.0" mmkv = "1.2.11" arouter = "1.5.2" okhttp = "4.9.0" navigation = "2.3.5" logger = "2.2.0" gson = "2.8.7" appcompat = "1.6.1" fragment = "1.5.5" activity = "1.6.1" support = "1.0.0" hibinding = "1.2.0" adapterhelper = "3.0.4" pickerview = "4.1.9" photoview = "2.3.0" glide = "4.12.0" latest = "latest.integration" kehud = "1.3.5" kepermission = "v1.0.0" kemvvm = "1.1.3" keimagepicker = "1.0.1" kewechat = "1.1.8" keqrscanner = "1.1" keaddresspicker = "1.0.5" keapkinstaller = "1.0.0" kefilepicker = "1.0.0" keidcard = "1.0.6" room = "2.5.1" [libraries] appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } material = { module = "com.google.android.material:material", version.ref = "material" } constraint-layout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "ktx" } navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" } navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" } fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" } activity = { module = "androidx.activity:activity", version.ref = "activity" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } support-v4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "support" } room-runtime = { module = "androidx.room:room-runtime",version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler",version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx",version.ref = "room" } hilt-android = { module = "com.google.dagger:hilt-android" , version.ref = "hilt"} hilt-compiler = { module = "com.google.dagger:hilt-compiler" , version.ref = "hilt"} glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } ke-mvvm = { module = "com.github.keluokeda:mvvm_base", version.ref = "kemvvm" } logger = { module = "com.orhanobut:logger", version.ref = "logger" } hi-binding = { module = "com.hi-dhl:binding", version.ref = "hibinding" } mmkv = { module = "com.tencent:mmkv-static", version.ref = "mmkv" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } moshi = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } adapter-helper = { module = "com.github.CymChad:BaseRecyclerViewAdapterHelper", version.ref = "adapterhelper" } ================================================ FILE: module/.gitignore ================================================ /build ================================================ FILE: module/build.gradle ================================================ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'com.google.dagger.hilt.android' id 'kotlin-parcelize' } kapt { correctErrorTypes true } android { compileSdk libs.versions.compilesdk.get().toInteger() defaultConfig { minSdk libs.versions.minsdk.get().toInteger() targetSdk libs.versions.targetsdk.get().toInteger() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" javaCompileOptions { annotationProcessorOptions { arguments += [ "room.schemaLocation":"$projectDir/schemas".toString(), "room.incremental":"true", "room.expandProjection":"true"] } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } resourcePrefix 'module_' buildFeatures { viewBinding = true } namespace 'com.ke.hs_tracker.module' } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.junit.jupiter:junit-jupiter' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation(libs.core.ktx) implementation(libs.appcompat) implementation(libs.material) implementation(libs.constraint.layout) implementation(libs.fragment.ktx) implementation(libs.activity) implementation(libs.lifecycle.viewmodel.ktx) implementation(libs.lifecycle.livedata.ktx) implementation(libs.lifecycle.runtime.ktx) implementation(libs.support.v4) implementation(libs.logger) implementation(libs.hilt.android) kapt(libs.hilt.compiler) implementation(libs.glide) kapt(libs.glide.compiler) implementation(libs.ke.mvvm) implementation(libs.adapter.helper) implementation(libs.retrofit) implementation(libs.retrofit.converter.moshi) implementation(libs.moshi) kapt(libs.moshi.codegen) implementation(libs.hi.binding) implementation(libs.mmkv) implementation(libs.room.runtime) implementation(libs.room.ktx) kapt(libs.room.compiler) // implementation 'com.tencent.bugly:crashreport:latest.release' implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' // optional - helpers for implementing LifecycleOwner in a Service implementation "androidx.lifecycle:lifecycle-service:2.6.1" } ================================================ FILE: module/consumer-rules.pro ================================================ ================================================ FILE: module/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: module/schemas/com.ke.hs_tracker.module.db.Database/1.json ================================================ { "formatVersion": 1, "database": { "version": 1, "identityHash": "d83bd3696913c5f4307118a1060d27e4", "entities": [ { "tableName": "card", "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`))", "fields": [ { "fieldPath": "artist", "columnName": "artist", "affinity": "TEXT", "notNull": false }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cost", "columnName": "cost", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbfId", "columnName": "dbfId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "text", "columnName": "text", "affinity": "TEXT", "notNull": true }, { "fieldPath": "set", "columnName": "set", "affinity": "TEXT", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cardClass", "columnName": "cardClass", "affinity": "TEXT", "notNull": false }, { "fieldPath": "classes", "columnName": "classes", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "mechanics", "columnName": "mechanics", "affinity": "TEXT", "notNull": true }, { "fieldPath": "flavor", "columnName": "flavor", "affinity": "TEXT", "notNull": true }, { "fieldPath": "rarity", "columnName": "rarity", "affinity": "TEXT", "notNull": false }, { "fieldPath": "durability", "columnName": "durability", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "armor", "columnName": "armor", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "collectible", "columnName": "collectible", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "spellSchool", "columnName": "spellSchool", "affinity": "TEXT", "notNull": false }, { "fieldPath": "race", "columnName": "race", "affinity": "TEXT", "notNull": false }, { "fieldPath": "attach", "columnName": "attach", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "health", "columnName": "health", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "game", "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)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "buildNumber", "columnName": "build_number", "affinity": "TEXT", "notNull": true }, { "fieldPath": "gameType", "columnName": "game_type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "formatType", "columnName": "format_type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "scenarioID", "columnName": "scenario_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userName", "columnName": "user_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "opponentName", "columnName": "opponent_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isUserFirst", "columnName": "is_user_first", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "userDeckName", "columnName": "user_deck_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userDeckCode", "columnName": "user_deck_code", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isUserWin", "columnName": "is_user_win", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "userHero", "columnName": "user_hero", "affinity": "TEXT", "notNull": false }, { "fieldPath": "opponentHero", "columnName": "opponent_class", "affinity": "TEXT", "notNull": false }, { "fieldPath": "startTime", "columnName": "start_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "endTime", "columnName": "end_time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd83bd3696913c5f4307118a1060d27e4')" ] } } ================================================ FILE: module/schemas/com.ke.hs_tracker.module.db.Database/2.json ================================================ { "formatVersion": 1, "database": { "version": 2, "identityHash": "1bb7b04a91def09f83b0a81252d181ba", "entities": [ { "tableName": "card", "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`))", "fields": [ { "fieldPath": "artist", "columnName": "artist", "affinity": "TEXT", "notNull": false }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cost", "columnName": "cost", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbfId", "columnName": "dbfId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "text", "columnName": "text", "affinity": "TEXT", "notNull": true }, { "fieldPath": "set", "columnName": "set", "affinity": "TEXT", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cardClass", "columnName": "cardClass", "affinity": "TEXT", "notNull": false }, { "fieldPath": "classes", "columnName": "classes", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "mechanics", "columnName": "mechanics", "affinity": "TEXT", "notNull": true }, { "fieldPath": "flavor", "columnName": "flavor", "affinity": "TEXT", "notNull": true }, { "fieldPath": "rarity", "columnName": "rarity", "affinity": "TEXT", "notNull": false }, { "fieldPath": "durability", "columnName": "durability", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "armor", "columnName": "armor", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "collectible", "columnName": "collectible", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "spellSchool", "columnName": "spellSchool", "affinity": "TEXT", "notNull": false }, { "fieldPath": "race", "columnName": "race", "affinity": "TEXT", "notNull": false }, { "fieldPath": "attach", "columnName": "attach", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "health", "columnName": "health", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "game", "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)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "buildNumber", "columnName": "build_number", "affinity": "TEXT", "notNull": true }, { "fieldPath": "gameType", "columnName": "game_type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "formatType", "columnName": "format_type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "scenarioID", "columnName": "scenario_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userName", "columnName": "user_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "opponentName", "columnName": "opponent_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isUserFirst", "columnName": "is_user_first", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "userDeckName", "columnName": "user_deck_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userDeckCode", "columnName": "user_deck_code", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isUserWin", "columnName": "is_user_win", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "userHero", "columnName": "user_hero", "affinity": "TEXT", "notNull": false }, { "fieldPath": "opponentHero", "columnName": "opponent_class", "affinity": "TEXT", "notNull": false }, { "fieldPath": "startTime", "columnName": "start_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "endTime", "columnName": "end_time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] }, { "tableName": "in_game_card", "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)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "buildNumber", "columnName": "build_number", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cardId", "columnName": "card_id", "affinity": "TEXT", "notNull": false }, { "fieldPath": "entityId", "columnName": "entity_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "position", "columnName": "position", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "zone", "columnName": "zone", "affinity": "TEXT", "notNull": true }, { "fieldPath": "turn", "columnName": "turn", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "isUser", "columnName": "is_user", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": true }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1bb7b04a91def09f83b0a81252d181ba')" ] } } ================================================ FILE: module/schemas/com.ke.hs_tracker.module.db.Database/3.json ================================================ { "formatVersion": 1, "database": { "version": 3, "identityHash": "720ca460a50c2d8a7f6bf96c98cf4358", "entities": [ { "tableName": "card", "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`))", "fields": [ { "fieldPath": "artist", "columnName": "artist", "affinity": "TEXT", "notNull": false }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cost", "columnName": "cost", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbfId", "columnName": "dbfId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "text", "columnName": "text", "affinity": "TEXT", "notNull": true }, { "fieldPath": "set", "columnName": "set", "affinity": "TEXT", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cardClass", "columnName": "cardClass", "affinity": "TEXT", "notNull": false }, { "fieldPath": "classes", "columnName": "classes", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "mechanics", "columnName": "mechanics", "affinity": "TEXT", "notNull": true }, { "fieldPath": "flavor", "columnName": "flavor", "affinity": "TEXT", "notNull": true }, { "fieldPath": "rarity", "columnName": "rarity", "affinity": "TEXT", "notNull": false }, { "fieldPath": "durability", "columnName": "durability", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "armor", "columnName": "armor", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "collectible", "columnName": "collectible", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "spellSchool", "columnName": "spellSchool", "affinity": "TEXT", "notNull": false }, { "fieldPath": "race", "columnName": "race", "affinity": "TEXT", "notNull": false }, { "fieldPath": "attach", "columnName": "attach", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "health", "columnName": "health", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "game", "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`))", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "buildNumber", "columnName": "build_number", "affinity": "TEXT", "notNull": true }, { "fieldPath": "gameType", "columnName": "game_type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "formatType", "columnName": "format_type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "scenarioID", "columnName": "scenario_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userName", "columnName": "user_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "opponentName", "columnName": "opponent_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isUserFirst", "columnName": "is_user_first", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "userDeckName", "columnName": "user_deck_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userDeckCode", "columnName": "user_deck_code", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isUserWin", "columnName": "is_user_win", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "userHero", "columnName": "user_hero", "affinity": "TEXT", "notNull": false }, { "fieldPath": "opponentHero", "columnName": "opponent_class", "affinity": "TEXT", "notNull": false }, { "fieldPath": "startTime", "columnName": "start_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "endTime", "columnName": "end_time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "zone_position_updated_event", "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`))", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "gameId", "columnName": "game_id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "entityId", "columnName": "entity_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "cardId", "columnName": "card_id", "affinity": "TEXT", "notNull": false }, { "fieldPath": "cardName", "columnName": "card_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "isUser", "columnName": "is_user", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "currentZone", "columnName": "current_zone", "affinity": "TEXT", "notNull": true }, { "fieldPath": "newZone", "columnName": "new_zone", "affinity": "TEXT", "notNull": true }, { "fieldPath": "currentPosition", "columnName": "current_position", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "newPosition", "columnName": "new_position", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '720ca460a50c2d8a7f6bf96c98cf4358')" ] } } ================================================ FILE: module/schemas/com.ke.hs_tracker.module.db.Database/4.json ================================================ { "formatVersion": 1, "database": { "version": 4, "identityHash": "a2944b7e136b6abe232a7a446c5ea3db", "entities": [ { "tableName": "card", "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`))", "fields": [ { "fieldPath": "artist", "columnName": "artist", "affinity": "TEXT", "notNull": false }, { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cost", "columnName": "cost", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "dbfId", "columnName": "dbfId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "text", "columnName": "text", "affinity": "TEXT", "notNull": true }, { "fieldPath": "set", "columnName": "set", "affinity": "TEXT", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "cardClass", "columnName": "cardClass", "affinity": "TEXT", "notNull": false }, { "fieldPath": "classes", "columnName": "classes", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "mechanics", "columnName": "mechanics", "affinity": "TEXT", "notNull": true }, { "fieldPath": "flavor", "columnName": "flavor", "affinity": "TEXT", "notNull": true }, { "fieldPath": "rarity", "columnName": "rarity", "affinity": "TEXT", "notNull": false }, { "fieldPath": "durability", "columnName": "durability", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "armor", "columnName": "armor", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "collectible", "columnName": "collectible", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "spellSchool", "columnName": "spellSchool", "affinity": "TEXT", "notNull": false }, { "fieldPath": "race", "columnName": "race", "affinity": "TEXT", "notNull": false }, { "fieldPath": "attack", "columnName": "attack", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "health", "columnName": "health", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "game", "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`))", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "buildNumber", "columnName": "build_number", "affinity": "TEXT", "notNull": true }, { "fieldPath": "gameType", "columnName": "game_type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "formatType", "columnName": "format_type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "scenarioID", "columnName": "scenario_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "userName", "columnName": "user_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "opponentName", "columnName": "opponent_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isUserFirst", "columnName": "is_user_first", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "userDeckName", "columnName": "user_deck_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "userDeckCode", "columnName": "user_deck_code", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isUserWin", "columnName": "is_user_win", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "userHero", "columnName": "user_hero", "affinity": "TEXT", "notNull": false }, { "fieldPath": "opponentHero", "columnName": "opponent_class", "affinity": "TEXT", "notNull": false }, { "fieldPath": "startTime", "columnName": "start_time", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "endTime", "columnName": "end_time", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] }, { "tableName": "zone_position_updated_event", "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`))", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "gameId", "columnName": "game_id", "affinity": "TEXT", "notNull": true }, { "fieldPath": "entityId", "columnName": "entity_id", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "cardId", "columnName": "card_id", "affinity": "TEXT", "notNull": false }, { "fieldPath": "cardName", "columnName": "card_name", "affinity": "TEXT", "notNull": false }, { "fieldPath": "isUser", "columnName": "is_user", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "currentZone", "columnName": "current_zone", "affinity": "TEXT", "notNull": true }, { "fieldPath": "newZone", "columnName": "new_zone", "affinity": "TEXT", "notNull": true }, { "fieldPath": "currentPosition", "columnName": "current_position", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "newPosition", "columnName": "new_position", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "id" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a2944b7e136b6abe232a7a446c5ea3db')" ] } } ================================================ FILE: module/src/androidTest/java/com/ke/hs_tracker/module/ExampleInstrumentedTest.kt ================================================ package com.ke.hs_tracker.module import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.* /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.ke.hs_tracker.module.test", appContext.packageName) } } ================================================ FILE: module/src/main/AndroidManifest.xml ================================================ ================================================ FILE: module/src/main/assets/Decks.log ================================================ I 22:23:04.0780690 Deck Contents Received: I 22:23:04.0780690 ### 狗贼 I 22:23:04.0780690 # Deck ID: 7996212750 I 22:23:04.0780690 AAEBAZICCukB6awCtLsC1pkDiLED6LoDndgDv+ADpu8DiYsECooOoM0CmNICntIChOYCv/ICj/YCm84D1OgDr4AEAA== I 22:23:04.0780690 ### 法术 I 22:23:04.0780690 # Deck ID: 7997355263 I 22:23:04.0780690 AAECAf0ECIy5A+DMA7/gA5PhA7L3A6CKBJiNBPyeBAvBuAOBvwPNzgP73QPr3gPQ7APR7AOn9wOKjQT9ngT7ogQA I 22:23:04.0780690 ### 自定义 潜行者 I 22:23:04.0780690 # Deck ID: 8003374434 I 22:23:04.0780690 AAECAaIHAAAA I 22:23:04.0780690 ### 小丑 I 22:23:04.0780690 # Deck ID: 8004144061 I 22:23:04.0780690 AAECAZICBrrQA/zeA6bvA9D5A4mLBOSkBAzougObzgPw1AP+2wOJ4AOV4AOi4QOk4QPR4QPm4QPe7AOvgAQA I 22:23:04.0780690 ### 双古神 I 22:23:04.0780690 # Deck ID: 8005951524 I 22:23:04.0780690 AAECAZICCO66A53YA/zeA4rgA6bvA9D5A4mLBKWNBAvougObzgPw1AOJ4AOV4AOi4QOk4QPR4QPm4QOP5AOvgAQA I 22:23:04.0780690 ### 圣契骑 I 22:23:04.0780690 # Deck ID: 8006192896 I 22:23:04.0780690 AAECAZ8FDOu5A+u5A+y5A+y5A4TBA5XNA5PQA7/RA/voA5HsA9n5A+CLBAn9uAPquQPKwQPA0QPM6wPw9gON+AO2gAT5pAQA I 22:23:04.0780690 ### 对决套牌 I 22:23:04.0780690 # Deck ID: 8009171311 I 22:23:04.0780690 AAEBAbLwAw+WBc3OA6TRA9nRA7fSA4vnA9DsA9HsA6f3A673A6CKBIqNBP2eBMWgBPuiBAAA I 22:23:04.0780690 ### 任务 I 22:23:04.0780690 # Deck ID: 8010668041 I 22:23:04.0780690 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA I 22:23:04.0780690 ### 卡扎库 I 22:23:04.0780690 # Deck ID: 8020766665 I 22:23:04.0780690 AAECAbr5AwPR4QOJiwSJnwQN5boD6LoD77oDm84D8NQDieADiuADjOQDj+QDr4AErp8EsKUEz6wEAA== I 22:23:06.5584800 Deck Contents Received: I 22:23:06.5584800 ### 狗贼 I 22:23:06.5584800 # Deck ID: 7996212750 I 22:23:06.5584800 AAEBAZICCukB6awCtLsC1pkDiLED6LoDndgDv+ADpu8DiYsECooOoM0CmNICntIChOYCv/ICj/YCm84D1OgDr4AEAA== I 22:23:06.5584800 ### 法术 I 22:23:06.5584800 # Deck ID: 7997355263 I 22:23:06.5584800 AAECAf0ECIy5A+DMA7/gA5PhA7L3A6CKBJiNBPyeBAvBuAOBvwPNzgP73QPr3gPQ7APR7AOn9wOKjQT9ngT7ogQA I 22:23:06.5584800 ### 自定义 潜行者 I 22:23:06.5584800 # Deck ID: 8003374434 I 22:23:06.5584800 AAECAaIHAAAA I 22:23:06.5584800 ### 小丑 I 22:23:06.5584800 # Deck ID: 8004144061 I 22:23:06.5584800 AAECAZICBrrQA/zeA6bvA9D5A4mLBOSkBAzougObzgPw1AP+2wOJ4AOV4AOi4QOk4QPR4QPm4QPe7AOvgAQA I 22:23:06.5584800 ### 双古神 I 22:23:06.5584800 # Deck ID: 8005951524 I 22:23:06.5584800 AAECAZICCO66A53YA/zeA4rgA6bvA9D5A4mLBKWNBAvougObzgPw1AOJ4AOV4AOi4QOk4QPR4QPm4QOP5AOvgAQA I 22:23:06.5584800 ### 圣契骑 I 22:23:06.5584800 # Deck ID: 8006192896 I 22:23:06.5584800 AAECAZ8FDOu5A+u5A+y5A+y5A4TBA5XNA5PQA7/RA/voA5HsA9n5A+CLBAn9uAPquQPKwQPA0QPM6wPw9gON+AO2gAT5pAQA I 22:23:06.5584800 ### 对决套牌 I 22:23:06.5584800 # Deck ID: 8009171311 I 22:23:06.5584800 AAEBAbLwAw+WBc3OA6TRA9nRA7fSA4vnA9DsA9HsA6f3A673A6CKBIqNBP2eBMWgBPuiBAAA I 22:23:06.5584800 ### 任务 I 22:23:06.5584800 # Deck ID: 8010668041 I 22:23:06.5584800 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA I 22:23:06.5584800 ### 卡扎库 I 22:23:06.5584800 # Deck ID: 8020766665 I 22:23:06.5584800 AAECAbr5AwPR4QOJiwSJnwQN5boD6LoD77oDm84D8NQDieADiuADjOQDj+QDr4AErp8EsKUEz6wEAA== I 22:23:35.6401730 Finding Game With Deck: I 22:23:35.6401730 ### 任务 I 22:23:35.6401730 # Deck ID: 8010668041 I 22:23:35.6401730 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA ================================================ FILE: module/src/main/assets/log.config ================================================ [Bob] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [Power] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [Achievements] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [Arena] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [FullScreenFX] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [LoadingScreen] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [Rachelle] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [Asset] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [Zone] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True [Decks] LogLevel=1 FilePrinting=True ConsolePrinting=False ScreenPrinting=False Verbose=True ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/MainApplication.kt ================================================ package com.ke.hs_tracker.module import android.app.Application import android.content.Context import android.net.Uri import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.res.ResourcesCompat import androidx.documentfile.provider.DocumentFile import com.bumptech.glide.Glide import com.ke.hs_tracker.module.data.PreferenceStorage import com.ke.hs_tracker.module.databinding.ModuleDialogCardPreviewBinding import com.ke.hs_tracker.module.databinding.ModuleItemCardBinding import com.ke.hs_tracker.module.entity.Card import com.ke.hs_tracker.module.parser.PowerParserImpl import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.Logger import com.orhanobut.logger.PrettyFormatStrategy import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.Calendar import java.util.Date import javax.inject.Inject abstract class MainApplication : Application() { @Inject lateinit var preferenceStorage: PreferenceStorage override fun onCreate() { super.onCreate() // CrashReport.initCrashReport(applicationContext, "abb84be20b", false) AppCompatDelegate.setDefaultNightMode(preferenceStorage.theme) Logger.addLogAdapter( AndroidLogAdapter( PrettyFormatStrategy.newBuilder() .methodCount(5) .build() ) ) } } fun String.removeTime(): Triple { val content = substring(PowerParserImpl.TIME_PREFIX_SIZE) val start = substring(0, 1) val hms = substring(2, 10).split(":") val calendar = Calendar.getInstance() calendar.set( Calendar.HOUR_OF_DAY, hms[0].toInt() ) calendar.set( Calendar.MINUTE, hms[1].toInt() ) calendar.set( Calendar.SECOND, hms[2].toInt() ) return Triple( start, calendar.time, content ) } fun String.log() { Logger.d(this) } /** * 是否具备所有权限 */ val Context.hasAllPermissions: Boolean get() { // val canWriteExternalStorage = ActivityCompat.checkSelfPermission( // this, // Manifest.permission.WRITE_EXTERNAL_STORAGE // ) == PackageManager.PERMISSION_GRANTED // // return canWriteExternalStorage && canReadDataDir && isExternalStorageManager() return canReadDataDir } /** * 是否可以访问data目录 */ val Context.canReadDataDir: Boolean get() { return DocumentFile.fromTreeUri(applicationContext, HS_DATA_FILE_DIR)?.canRead() ?: false } /** * 是否具备外部存储管理权限,如果android版本低于11就返回true */ //fun isExternalStorageManager(): Boolean { // // return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Environment.isExternalStorageManager() // } else { // return true // } //} const val HS_APPLICATION_ID = "com.blizzard.wtcg.hearthstone" val HS_DATA_FILE_DIR = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata%2F${HS_APPLICATION_ID}")!! //const val HUAWEI_HS_APPLICATION_ID = "com.blizzard.wtcg.hearthstone.cn.huawei" /** * 写入log.config文件 */ suspend fun Context.writeLogConfigFile(forceWrite: Boolean = false): Boolean { return withContext(Dispatchers.IO) { val documentFile = findHSDataFilesDir() ?: return@withContext false val fileName = "log.config" val file = findHSDataFilesDir(fileName) if (file != null) { //文件已存在 "log.config文件已存在".log() if (forceWrite) { file.delete() } else { return@withContext true } } val configFile = documentFile.createFile("plain/text", fileName) ?: return@withContext false contentResolver.openOutputStream(configFile.uri)?.use { assets.open("log.config") .copyTo(it) it.flush() } return@withContext true } } //通过从根目录进入的方式可以创建文件 fun Context.findHSDataFilesDir( fileName: String? = null, ): DocumentFile? { DocumentFile.fromTreeUri( applicationContext, HS_DATA_FILE_DIR )?.apply { // listFiles().forEach { // if (it.name == applicationId) { val filesDir = this.findFile("files") ?: return null return if (fileName == null) filesDir else filesDir.findFile( fileName ) // } // } } return null // if (applicationId == HUAWEI_HS_APPLICATION_ID) { // return null // } // // return findHSDataFilesDir(fileName, HUAWEI_HS_APPLICATION_ID) } fun ModuleItemCardBinding.bindCard(card: Card) { name.text = card.name cost.text = card.cost.toString() card.rarity?.apply { this@bindCard.cost.setBackgroundColor( ResourcesCompat.getColor( root.context.resources, colorRes, null ) ) } Glide.with(imageTile) .load("https://art.hearthstonejson.com/v1/tiles/${card.id}.png") .into(imageTile) } fun showCardImageDialog(context: Context, cardId: String) { val binding = ModuleDialogCardPreviewBinding.inflate(LayoutInflater.from(context)) AlertDialog.Builder(context) .show().apply { window?.run { setContentView(binding.root) binding.root.setOnClickListener { dismiss() } //去掉对话框的白色背景 setBackgroundDrawableResource(android.R.color.transparent) } } Glide.with(binding.image) .load("https://art.hearthstonejson.com/v1/render/latest/zhCN/512x/${cardId}.png") .placeholder(R.mipmap.ic_launcher) .into(binding.image) } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/api/HearthStoneJsonApi.kt ================================================ package com.ke.hs_tracker.module.api import com.ke.hs_tracker.module.entity.Card import retrofit2.http.GET import retrofit2.http.Path interface HearthStoneJsonApi { /** * 获取卡牌数据 */ @GET("v1/{versionCode}/{region}/cards.json") suspend fun getCardJsonList( @Path("versionCode") versionCode: String, @Path("region") region: String, ): List companion object { const val BASE_URL = "https://api.hearthstonejson.com/" } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/data/PreferenceStorage.kt ================================================ package com.ke.hs_tracker.module.data import android.content.Context import androidx.appcompat.app.AppCompatDelegate import com.tencent.mmkv.MMKV import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject interface PreferenceStorage { var theme: Int /** * 保存日志文件 */ var saveLogFile: Boolean var floatingEnable: Boolean var crash: String? } class PreferenceStorageImpl @Inject constructor( @ApplicationContext private val context: Context ) : PreferenceStorage { init { MMKV.initialize(context) } private val mmkv = MMKV.defaultMMKV() override var theme: Int get() = mmkv.getInt(KEY_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) set(value) { AppCompatDelegate.setDefaultNightMode(value) mmkv.encode(KEY_THEME, value) } override var saveLogFile: Boolean get() = mmkv.getBoolean(KEY_SAVE_LOG_FILE, false) set(value) { mmkv.encode(KEY_SAVE_LOG_FILE, value) } override var floatingEnable: Boolean get() = mmkv.getBoolean(KEY_FLOATING_ENABLE, true) set(value) { mmkv.putBoolean(KEY_FLOATING_ENABLE, value) } override var crash: String? get() = mmkv.getString(KEY_CRASH, null) set(value) { mmkv.putString(KEY_CRASH, value) } companion object { private const val KEY_THEME = "KEY_THEME" private const val KEY_SAVE_LOG_FILE = "KEY_SAVE_LOG_FILE" private const val KEY_FLOATING_ENABLE = "KEY_FLOATING_ENABLE" private const val KEY_CRASH = "KEY_CRASH" } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/db/CardClassesConvert.kt ================================================ package com.ke.hs_tracker.module.db import androidx.room.TypeConverter import com.ke.hs_tracker.module.entity.CardClass class CardClassesConvert { @TypeConverter fun longToClasses(value: Long?): List { if (value == null) { return emptyList() } return CardClass.values() .map { it to (1L shl it.ordinal) } .filter { it.second and value == it.second } .map { it.first } } @TypeConverter fun classesToLong(list: List): Long { if (list.isEmpty()) { return 0 } var result = 0L list.map { 1L shl it.ordinal }.forEach { result = result or it } return result } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/db/CardDao.kt ================================================ package com.ke.hs_tracker.module.db import androidx.room.Dao import androidx.room.Insert import androidx.room.Query import com.ke.hs_tracker.module.entity.Card @Dao interface CardDao { @Query("select * from card") suspend fun getAll(): List @Insert suspend fun insert(list: List) @Query("delete from card") suspend fun deleteAll() @Query("select COUNT(id) from card") suspend fun getCount(): Int } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/db/Database.kt ================================================ package com.ke.hs_tracker.module.db import androidx.room.* import androidx.room.Database import androidx.room.migration.AutoMigrationSpec import com.ke.hs_tracker.module.entity.Card const val DATABASE_VERSION = 4 @Database( entities = [Card::class, Game::class, ZonePositionChangedEvent::class], version = DATABASE_VERSION, exportSchema = true, autoMigrations = [ AutoMigration( from = 3, to = 4, spec = com.ke.hs_tracker.module.db.Database.RenameAttachToAttackMigration::class ) ] ) @TypeConverters( CardClassesConvert::class, MechanicsListConvert::class ) abstract class Database : RoomDatabase() { @RenameColumn(tableName = "card", fromColumnName = "attach", toColumnName = "attack") class RenameAttachToAttackMigration : AutoMigrationSpec abstract fun cardDao(): CardDao abstract fun gameDao(): GameDao abstract fun zonePositionChangedEventDao(): ZonePositionChangedEventDao companion object { const val VERSION = 3 } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/db/Game.kt ================================================ package com.ke.hs_tracker.module.db import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import com.ke.hs_tracker.module.entity.CardClass import com.ke.hs_tracker.module.entity.FormatType import com.ke.hs_tracker.module.entity.GameType import com.squareup.moshi.JsonClass import java.util.* @JsonClass(generateAdapter = true) @Entity(tableName = "game") data class Game( @PrimaryKey(autoGenerate = false) val id: String = UUID.randomUUID().toString(), //并不是唯一标识 @ColumnInfo(name = "build_number") val buildNumber: String = "", @ColumnInfo(name = "game_type") var gameType: GameType = GameType.Unknown, @ColumnInfo(name = "format_type") var formatType: FormatType = FormatType.Unknown, @ColumnInfo(name = "scenario_id") var scenarioID: Int = 0, @ColumnInfo(name = "user_name") var userName: String = "", @ColumnInfo(name = "opponent_name") var opponentName: String = "", @ColumnInfo(name = "is_user_first") var isUserFirst: Boolean? = null, @ColumnInfo(name = "user_deck_name") var userDeckName: String = "", @ColumnInfo(name = "user_deck_code") var userDeckCode: String = "", @ColumnInfo(name = "is_user_win") var isUserWin: Boolean? = null, @ColumnInfo(name = "user_hero") var userHero: CardClass? = null, @ColumnInfo(name = "opponent_class") var opponentHero: CardClass? = null, @ColumnInfo(name = "start_time") var startTime: Long = 0, @ColumnInfo(name = "end_time") var endTime: Long = 0 ) { // val userName: String // get() = if (isUserFirst == true) player1Name else player2Name // var opponentName: String // get() = if (isUserFirst == true) player2Name else player1Name // set(value) { // if (isUserFirst == true) { // player2Name = value // } else { // player1Name = value // } // } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/db/GameDao.kt ================================================ package com.ke.hs_tracker.module.db import androidx.room.* import com.ke.hs_tracker.module.entity.CardClass @Dao interface GameDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(game: Game) /** * 插入所有 */ @Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insert(games: List) @Update suspend fun update(game: Game) /** * 删除全部 */ @Query("delete from game") suspend fun deleteAll() /** * 查询所有 */ @Query("select * from game") suspend fun getAll(): List /** * 查找用户某个英雄的总对局 */ @Query("select * from game where user_hero = :cardClass") suspend fun getByHero(cardClass: CardClass): List /** * 获取总的对局数 */ @Query("select count(*) from game") suspend fun getGameCount(): Int /** * 获取玩家胜率对局数 */ @Query("select count(*) from game where is_user_win = 1") suspend fun getUserWinCount(): Int /** * 根据卡组代码和名称查询 */ @Query("select * from game where user_deck_code = :code and user_deck_name = :name") suspend fun getByDeckCodeAndName( code: String, name: String ): List } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/db/MechanicsListConvert.kt ================================================ package com.ke.hs_tracker.module.db import androidx.room.TypeConverter import com.ke.hs_tracker.module.entity.CardClass import com.ke.hs_tracker.module.entity.Mechanics import com.ke.hs_tracker.module.entity.MechanicsAdapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import org.json.JSONArray import javax.inject.Inject class MechanicsListConvert { @TypeConverter fun stringToClasses(value: String?): List { if (value == null) { return emptyList() } val result = mutableListOf() val jsonArray = JSONArray(value) val mechanicsAdapter = MechanicsAdapter() val size = jsonArray.length() for (index in 0 until size) { val m = mechanicsAdapter.fromJson(jsonArray.get(index).toString()) result.add(m) } return result // return Mechanics.values() // .map { // it to (1L shl it.ordinal) // } // .filter { // it.second and value == it.second // } // .map { it.first } } @TypeConverter fun classesToString(list: List): String { val jsonArray = JSONArray() if (list.isEmpty()) { return jsonArray.toString() } list.forEach { jsonArray.put(it.name) } return jsonArray.toString() } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/db/ZonePositionChangedEvent.kt ================================================ package com.ke.hs_tracker.module.db import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import com.ke.hs_tracker.module.entity.Zone import com.squareup.moshi.JsonClass import java.util.* @JsonClass(generateAdapter = true) @Entity(tableName = "zone_position_updated_event") data class ZonePositionChangedEvent( @PrimaryKey(autoGenerate = false) val id: String = UUID.randomUUID().toString(), @ColumnInfo(name = "game_id") var gameId: String = "", @ColumnInfo(name = "entity_id") val entityId: Int, @ColumnInfo(name = "card_id") var cardId: String?, @ColumnInfo(name = "card_name") var cardName: String? = null, @ColumnInfo(name = "is_user") var isUser: Boolean = false, @ColumnInfo(name = "current_zone") var currentZone: Zone, @ColumnInfo(name = "new_zone") var newZone: Zone, @ColumnInfo(name = "current_position") val currentPosition: Int, @ColumnInfo(name = "new_position") val newPosition: Int ) { //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] tag=ZONE value=HAND //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] tag=ZONE_POSITION value=1 fun plus(event: ZonePositionChangedEvent): ZonePositionChangedEvent { if (event.entityId != entityId) { throw RuntimeException("两个要进行加的 entity id 不一致 $event") } if (currentZone == Zone.Deck && newZone == Zone.Deck && newPosition != 0) { //星界导致插入一张卡牌到事件中 currentZone = event.newZone newZone = event.newZone } if (currentZone != event.currentZone) { throw RuntimeException("两个要进行加的 zone 不一致 $event") } val newPosAndZone = if (this.currentZone == event.newZone) event.newPosition to newZone else newPosition to event.newZone return ZonePositionChangedEvent( id, gameId, entityId, cardId, cardName, isUser, currentZone, newPosAndZone.second, currentPosition, newPosAndZone.first ) } fun plusPlus( second: ZonePositionChangedEvent, third: ZonePositionChangedEvent ): ZonePositionChangedEvent { val oldZone = currentZone val list = listOf(this, second, third) val newZone = list.map { it.newZone }.firstOrNull { it != oldZone } val newPosition = third.newPosition return ZonePositionChangedEvent( id, gameId, entityId, cardId, cardName, isUser, currentZone, newZone ?: this.newZone, currentPosition, newPosition ) } } //public operator fun ZonePositionChangedEvent.plus(event: ZonePositionChangedEvent): ZonePositionChangedEvent { // if (entityId != event.entityId) { // throw RuntimeException("不支持不同 entityId 的 event 相加") // } // if (this == event) { // return this // } //} ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/db/ZonePositionChangedEventDao.kt ================================================ package com.ke.hs_tracker.module.db import androidx.room.Dao import androidx.room.Insert import androidx.room.Query @Dao interface ZonePositionChangedEventDao { @Insert suspend fun insertAll(list: List) @Query("select * from zone_position_updated_event where game_id = :gameId") suspend fun getAllByGameId(gameId: String): List /** * 获取所有 */ @Query("select * from zone_position_updated_event") suspend fun getAll(): List } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/di/CoroutinesModule.kt ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ke.hs_tracker.module.di import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @InstallIn(SingletonComponent::class) @Module object CoroutinesModule { @DefaultDispatcher @Provides fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default @IoDispatcher @Provides fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO @MainDispatcher @Provides fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main @MainImmediateDispatcher @Provides fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/di/CoroutinesQualifiers.kt ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ke.hs_tracker.module.di import javax.inject.Qualifier @Retention(AnnotationRetention.BINARY) @Qualifier annotation class DefaultDispatcher @Retention(AnnotationRetention.BINARY) @Qualifier annotation class IoDispatcher @Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainDispatcher @Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainImmediateDispatcher @Retention(AnnotationRetention.BINARY) @Qualifier annotation class ApplicationScope ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/di/LogFileDirQualifiers.kt ================================================ /* * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ke.hs_tracker.module.di import javax.inject.Qualifier @Retention(AnnotationRetention.BINARY) @Qualifier annotation class LocalLogFileDir @Retention(AnnotationRetention.BINARY) @Qualifier annotation class RealLogFileDir ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/di/Module.kt ================================================ package com.ke.hs_tracker.module.di import android.content.Context import androidx.room.Room import com.ke.hs_tracker.module.api.HearthStoneJsonApi import com.ke.hs_tracker.module.data.PreferenceStorage import com.ke.hs_tracker.module.data.PreferenceStorageImpl import com.ke.hs_tracker.module.db.* import com.ke.hs_tracker.module.entity.* import com.ke.hs_tracker.module.parser.* import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class Module { @Provides @Singleton fun provideHttpClient(): OkHttpClient { return OkHttpClient.Builder() .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS) .build() } @Provides @Singleton fun provideMoshi(): Moshi { return Moshi.Builder() .add(CardClassAdapter()) .add(CardTypeAdapter()) .add(RarityAdapter()) .add(SpellSchoolAdapter()) .add(MechanicsAdapter()) .add(RaceAdapter()) .build() } @Provides @Singleton fun provideHearthStoneJsonApi( okHttpClient: OkHttpClient, moshi: Moshi ): HearthStoneJsonApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .client(okHttpClient) .baseUrl(HearthStoneJsonApi.BASE_URL) .build() .create(HearthStoneJsonApi::class.java) } @Provides @Singleton fun provideDatabase(@ApplicationContext context: Context): Database { return Room.databaseBuilder( context, Database::class.java, "card.db" ).build() } @Provides fun provideCardDao(database: Database): CardDao { return database.cardDao() } @Provides fun provideGameDao(database: Database): GameDao = database.gameDao() @Provides fun provideZonePositionChangedEventDao(database: Database): ZonePositionChangedEventDao = database.zonePositionChangedEventDao() @Provides @Singleton fun providePreferenceStorage(preferenceStorageImpl: PreferenceStorageImpl): PreferenceStorage { return preferenceStorageImpl } @Provides fun provideBlockTagStack(impl: BlockTagStackImpl): BlockTagStack { return impl } @Provides fun providePowerParser(powerParserImpl: PowerParserImpl): PowerParser { return powerParserImpl } @Provides fun providePowerTagHandler(powerTagHandlerImpl: PowerTagHandlerImpl): PowerTagHandler = powerTagHandlerImpl @Provides fun provideDeckCardObserver(deckCardObserverImpl: DeckCardObserverImpl): DeckCardObserver = deckCardObserverImpl } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/ClearCardTableUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import com.ke.hs_tracker.module.db.CardDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class ClearCardTableUseCase @Inject constructor( private val cardDao: CardDao, @IoDispatcher dispatcher: CoroutineDispatcher ) : UseCase(dispatcher) { override suspend fun execute(parameters: Unit) { cardDao.deleteAll() } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetAllCardUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import com.ke.hs_tracker.module.db.CardDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.entity.Card import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class GetAllCardUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, private val cardDao: CardDao ) : UseCase>(dispatcher) { override suspend fun execute(parameters: Unit): List { return cardDao.getAll() } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetCardListUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import com.ke.hs_tracker.module.api.HearthStoneJsonApi import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.entity.Card import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class GetCardListUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, private val api: HearthStoneJsonApi ) : UseCase, List>(dispatcher) { override suspend fun execute(parameters: Pair): List { return api.getCardJsonList( parameters.first, parameters.second ) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetDatabaseCardCountUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import com.ke.hs_tracker.module.db.CardDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class GetDatabaseCardCountUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, private val cardDao: CardDao ) : UseCase(dispatcher) { override suspend fun execute(parameters: Unit): Int { return cardDao.getCount() } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetLocalLogDirUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import android.content.Context import androidx.documentfile.provider.DocumentFile import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.findHSDataFilesDir import com.ke.mvvm.base.domian.UseCase import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import java.io.File import javax.inject.Inject class GetLocalLogDirUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, @ApplicationContext private val context: Context ) : UseCase(dispatcher) { override suspend fun execute(parameters: Unit): DocumentFile? { val logsDir = context.getExternalFilesDir("Logs")!! if (!logsDir.exists()) { logsDir.mkdir() } // val decksFile = File(logsDir, "Decks.log") // decksFile.createNewFile() // context.assets.open("Decks.log").reader() // .apply { // decksFile.writeText(readText()) // } val powerFile = File(logsDir, "Power.log") powerFile.createNewFile() return context.findHSDataFilesDir("Logs") } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetRealLogDirUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import android.content.Context import androidx.documentfile.provider.DocumentFile import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.findHSDataFilesDir import com.ke.hs_tracker.module.log import com.ke.mvvm.base.domian.UseCase import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class GetRealLogDirUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, @ApplicationContext private val context: Context ) : UseCase(dispatcher) { // private var documentFile: DocumentFile? = null override suspend fun execute(parameters: Unit): DocumentFile? { // if (documentFile != null) { // return documentFile // } val logsDir = context.findHSDataFilesDir("Logs") ?: return null val listFiles = logsDir.listFiles() return listFiles.filter { "${it.name} ${it.lastModified()}".log() (it.name?.startsWith("Hearthstone") ?: false) && it.isDirectory }.maxByOrNull { it.lastModified() }?.apply { "找到了目标目录 ${this.name} ${this.lastModified()}".log() } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetSaveLogFileEnableUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import com.ke.hs_tracker.module.data.PreferenceStorage import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class GetSaveLogFileEnableUseCase @Inject constructor( private val preferenceStorage: PreferenceStorage, @IoDispatcher dispatcher: CoroutineDispatcher ) : UseCase(dispatcher) { override suspend fun execute(parameters: Unit): Boolean { return preferenceStorage.saveLogFile } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/InsertCardListToDatabaseUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import com.ke.hs_tracker.module.db.CardDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.entity.Card import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class InsertCardListToDatabaseUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, private val cardDao: CardDao ) : UseCase, Unit>(dispatcher) { override suspend fun execute(parameters: List) { cardDao.insert(parameters) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/ParseDeckCodeUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import android.util.Base64 import com.ke.hs_tracker.module.db.CardDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.entity.Card import com.ke.hs_tracker.module.entity.CardBean import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class ParseDeckCodeUseCase @Inject constructor( private val cardDao: CardDao, @IoDispatcher dispatcher: CoroutineDispatcher ) : UseCase>(dispatcher) { override suspend fun execute(parameters: String): List { val allCards = cardDao.getAll() val byteArray = Base64.decode(parameters, Base64.DEFAULT) // deckString.encodeToByteArray().decodeBase64() //[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] // val size = byteArray.size val byteList = mutableListOf() byteArray.forEach { byteList.add(it) } val keep = byteList.removeFirst()//移除第一个保留的字段 // assert(byteList.removeFirst().toInt() == 0) // assert(byteList.removeFirst().toInt() != 0)//总是1 val version = byteList.removeFirst() val cardType = byteList.removeFirst()//1是标准 2是狂野 val cardList = mutableListOf() val heroCount = getVarInt(byteList) for (i in 0 until heroCount) { val id = getVarInt(byteList) val hero = findByDbfId(id, allCards) ?: throw IllegalArgumentException("找不到id为 $id 的卡牌") // hero.count = heroCount cardList.add(hero.updateCount(heroCount)) } for (i in 1..3) { val c = getVarInt(byteList) for (j in 0 until c) { val dbfId = getVarInt(byteList) val count: Int if (i == 3) { count = getVarInt(byteList) } else { count = i } // result.cards.add(Card(dbfId, count)) val jsonObject = findByDbfId(dbfId, allCards) ?: throw IllegalArgumentException("找不到id为 $dbfId 的卡牌") // jsonObject.count = count cardList.add(jsonObject.updateCount(count)) } } //移除英雄 cardList.removeFirst() cardList.sortBy { it.card.cost } return cardList } /** * 获得无符号int */ private fun getVarInt(src: MutableList): Int { var result = 0 var shift = 0 var b: Int do { if (shift >= 32) { // Out of range throw IndexOutOfBoundsException("varint too long") } // Get 7 bits from next byte b = src.removeFirst().toInt() result = result or (b and 0x7F shl shift) shift += 7 } while (b and 0x80 != 0) return result } private fun findByDbfId(id: Int, cardEntityList: List): CardBean? { val cardEntity = cardEntityList.find { it.dbfId == id } ?: return null return CardBean(cardEntity) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/SaveLogFileUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import android.content.Context import android.net.Uri import com.ke.hs_tracker.module.data.PreferenceStorage import com.ke.mvvm.base.domian.UseCase import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import java.io.File import java.io.InputStream import javax.inject.Inject class SaveLogFileUseCase @Inject constructor( private val preferenceStorage: PreferenceStorage, @ApplicationContext private val context: Context ) : UseCase, Boolean>(Dispatchers.IO) { override suspend fun execute(parameters: Pair): Boolean { if (!preferenceStorage.saveLogFile) { return false } val targetFileDir = File(context.getExternalFilesDir(null), "logs") if (!targetFileDir.exists()) { targetFileDir.mkdir() } val target = File(targetFileDir, parameters.first + ".log") if (!target.exists()) { target.createNewFile() } val text = parameters.second.bufferedReader().readText() target.writeText(text) return true } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/SetSaveLogFileEnableUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import com.ke.hs_tracker.module.data.PreferenceStorage import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class SetSaveLogFileEnableUseCase @Inject constructor( private val preferenceStorage: PreferenceStorage, @IoDispatcher dispatcher: CoroutineDispatcher ) : UseCase(dispatcher) { override suspend fun execute(parameters: Boolean): Boolean { preferenceStorage.saveLogFile = parameters return parameters } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/domain/WriteLogConfigFileUseCase.kt ================================================ package com.ke.hs_tracker.module.domain import android.content.Context import androidx.documentfile.provider.DocumentFile import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.findHSDataFilesDir import com.ke.hs_tracker.module.log import com.ke.hs_tracker.module.writeLogConfigFile import com.ke.mvvm.base.domian.UseCase import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class WriteLogConfigFileUseCase @Inject constructor( @ApplicationContext private val context: Context, @IoDispatcher private val dispatcher: CoroutineDispatcher ) : UseCase(dispatcher) { override suspend fun execute(parameters: Boolean): Boolean { return context.writeLogConfigFile(parameters) // val documentFile = context.findHSDataFilesDir() ?: return false // // val fileName = "log.config" // val file = context.findHSDataFilesDir(fileName) // if (file != null) { // //文件已存在 // "log.config文件已存在".log() // // if (parameters) { // file.delete() // val configFile = documentFile.createFile("plain/text", fileName) // ?: return false // // write(configFile) // } else { // return true // } // } // val configFile = documentFile.createFile("plain/text", fileName) // ?: return false // // write(configFile) // // return true } private fun write(configFile: DocumentFile) { context.contentResolver.openOutputStream(configFile.uri)?.use { context.assets.open("log.config") .copyTo(it) it.flush() } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/BlockType.kt ================================================ package com.ke.hs_tracker.module.entity enum class BlockType { /** * 攻击 */ Attack, /** * 死亡 */ Deaths, /** * 触发 */ Trigger, /** * 打出一张卡牌 */ Play, /** * 卡牌生效 */ Power, /** * 交易 */ Trade } internal fun String.toBlockType(fallback: BlockType = BlockType.Trigger): BlockType { BlockType.values().forEach { if (it.name.equals(this, true)) { return it } } return fallback } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Card.kt ================================================ package com.ke.hs_tracker.module.entity import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey import com.squareup.moshi.JsonClass import kotlinx.parcelize.Parcelize @Entity(tableName = "card") @JsonClass(generateAdapter = true) @Parcelize data class Card( /** * 画家 */ val artist: String? = null, /** * 名称 */ val name: String, /** * 费用 */ val cost: Int = 0, @PrimaryKey val id: String, val dbfId: Int, val text: String = "", //属于哪个版本 例如 TGT val set: String = "", val type: CardType = CardType.None, val cardClass: CardClass? = null, val classes: List = emptyList(), val mechanics: List = emptyList(), /** * 个性介绍 */ val flavor: String = "", val rarity: Rarity? = null, /** * 武器耐久 */ val durability: Int = 0, /** * 英雄牌的护甲 */ val armor: Int = 0, val collectible: Boolean = false, /** * 法术类型 */ val spellSchool: SpellSchool? = null, /** * 随从种族 */ val race: Race? = null, /** * 随从攻击力 */ val attack: Int = 0, /** * 随从生命值 */ val health: Int = 0, ) : Parcelable ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/CardBean.kt ================================================ package com.ke.hs_tracker.module.entity data class CardBean( val card: Card, val count: Int = 0 ) { //防止在使用MutableStateFlow时无法更新数据 // override fun equals(other: Any?): Boolean { // return false // } // // override fun hashCode(): Int { // return Random.nextInt() // } fun updateCount(count: Int): CardBean { return CardBean(card, count) } fun toCardList(): List { val list = mutableListOf() repeat(count) { list.add(card) } return list } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/CardClass.kt ================================================ package com.ke.hs_tracker.module.entity import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.ke.hs_tracker.module.R import com.squareup.moshi.FromJson import com.squareup.moshi.ToJson enum class CardClass( @StringRes val titleRes: Int, @ColorRes val color: Int = 0, @DrawableRes val roundIcon: Int? = null, val isHero: Boolean = true ) { /** * 法师 */ Mage(R.string.module_mage, R.color.module_mage, R.drawable.module_image_round_mage), /** * 术士 */ Warlock(R.string.module_warlock, R.color.module_warlock, R.drawable.module_image_round_warlock), /** * 牧师 */ Priest(R.string.module_priest, R.color.module_priest, R.drawable.module_image_round_priest), /** * 德鲁伊 */ Druid(R.string.module_druid, R.color.module_druid, R.drawable.module_image_round_druid), /** * 盗贼 */ Rogue(R.string.module_rogue, R.color.module_rogue, R.drawable.module_image_round_rogue), /** * 萨满 */ Shaman(R.string.module_shaman, R.color.module_shaman, R.drawable.module_image_round_shaman), /** * 猎人 */ Hunter(R.string.module_hunter, R.color.module_hunter, R.drawable.module_image_round_hunter), /** * 圣骑士 */ Paladin(R.string.module_paladin, R.color.module_paladin, R.drawable.module_image_round_paladin), /** * 战士 */ Warrior(R.string.module_warrior, R.color.module_warrior, R.drawable.module_image_round_warrior), /** * 恶魔猎手 */ DemonHunter( R.string.module_demon_hunter, R.color.module_demon_hunter, R.drawable.module_image_round_demon_hunter ), /** * 中立 */ Neutral(R.string.module_neutral, R.color.module_neutral, isHero = false), /** * 威兹班 */ Whizbang(R.string.module_whizbang, 0, isHero = false), /** * 梦境牌 */ Dream(R.string.module_dream, 0, isHero = false), /** * 死亡骑士 */ DeathKnight(R.string.module_death_knight, R.color.module_death_knight, isHero = false), } class CardClassAdapter { @FromJson fun fromJson(value: String): CardClass { return EnumMoshiAdapter.fromJson(value, CardClass.values()) } @ToJson fun toJson(cardClass: CardClass) = cardClass.name.uppercase() } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/CardType.kt ================================================ package com.ke.hs_tracker.module.entity import com.squareup.moshi.FromJson import com.squareup.moshi.ToJson enum class CardType { /** * 英雄 */ Hero, /** * 英雄技能 */ HeroPower, /** *衍生牌 */ Enchantment, /** * 法术 */ Spell, /** * 随从 */ Minion, /** * 武器 */ Weapon, None } class CardTypeAdapter { @FromJson fun fromJson(value: String): CardType { return CardType.values() .find { it.name.equals(value.replace("_", ""), true) } ?: CardType.None } @ToJson fun toJson(cardType: CardType) = cardType.name.uppercase() } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/CurrentDeck.kt ================================================ package com.ke.hs_tracker.module.entity data class CurrentDeck( val name: String, val code: String ) ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Entity.kt ================================================ package com.ke.hs_tracker.module.entity import com.ke.hs_tracker.module.parser.PowerParserImpl //有三种形式 //1,Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1] //2,Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1] //3,Entity=失落的裤子#5629 data class Entity( val entityName: String, val gameCardType: GameCardType? = null, val id: Int = -1, val zone: Zone = Zone.Play, val zonePosition: Int = -1, val cardId: String? = null, val player: Int = -1 ) { val entityType: EntityType get() = when { gameCardType == null && id == -1 && zone == Zone.Play && zonePosition == -1 && cardId == null && player == -1 -> EntityType.Name gameCardType == GameCardType.Invalid -> EntityType.Invalid else -> EntityType.Clear } val isGameEntity: Boolean get() = entityName == "GameEntity" val isUserEntity: Boolean get() = entityName.contains("#") companion object { internal fun createFromContent(content: String): Entity? { if (content == "0") { return null } var matchResult = PowerParserImpl.FULL_ENTITY_CONTENT1_PATTERN.matchEntire(content) if (matchResult != null) { return Entity( matchResult.groupValues[1], matchResult.groupValues[2].toCardType(GameCardType.Invalid), matchResult.groupValues[3].toIntOrNull() ?: 0, matchResult.groupValues[4].toZone(), matchResult.groupValues[5].toIntOrNull() ?: 0, matchResult.groupValues[6].ifBlank { null }, matchResult.groupValues[7].toIntOrNull() ?: 0 ) } matchResult = PowerParserImpl.FULL_ENTITY_CONTENT2_PATTERN.matchEntire(content) ?: return Entity(content) return Entity( matchResult.groupValues[1], GameCardType.Invalid, matchResult.groupValues[2].toIntOrNull() ?: 0, matchResult.groupValues[3].toZone(), matchResult.groupValues[4].toIntOrNull() ?: 0, matchResult.groupValues[5].ifBlank { null }, matchResult.groupValues[6].toIntOrNull() ?: 0 ) } } } enum class EntityType { //Entity=失落的裤子#5629 Name, //Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1] Invalid, //Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1] Clear } data class GameEntity( val gameCardType: GameCardType, val entityId: Int ) /** * 玩家 */ data class Player( val entityId: Int, val playerId: Int, val controller: Int, val gameCardType: GameCardType, val heroEntity: Int, /** * 手牌上限 */ val maxHandSize: Int, /** * 起始手牌 */ val startHandSize: Int, val teamId: Int, /** * 费用上限 一般为10 */ val maxResources: Int ) { companion object { internal fun fromMap(map: Map): Player { return Player( entityId = map["entityid"]?.toIntOrNull() ?: 0, playerId = map["playerid"]?.toIntOrNull() ?: 0, controller = map["controller"]?.toIntOrNull() ?: 0, gameCardType = (map["cardtype"] ?: "").toCardType(), heroEntity = map["heroentity"]?.toIntOrNull() ?: 0, maxHandSize = map["maxhandsize"]?.toIntOrNull() ?: 0, startHandSize = map["starthandsize"]?.toIntOrNull() ?: 0, teamId = map["teamid"]?.toIntOrNull() ?: 0, maxResources = map["maxresources"]?.toIntOrNull() ?: 0 ) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/EntityWithPayload.kt ================================================ package com.ke.hs_tracker.module.entity data class EntityWithPayload( val entity: Entity, private val payloads: MutableMap = mutableMapOf() ) { fun add(pair: Pair) { payloads[pair.first] = pair.second } fun getPayloads(): Map = payloads } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/EnumMoshiAdapter.kt ================================================ package com.ke.hs_tracker.module.entity object EnumMoshiAdapter { fun > fromJson(value: String, enumList: Array, fallback: T? = null): T { val enum = enumList .find { it.name.equals(value.replace("_", ""), true) } ?: fallback if (enum == null) { throw IllegalArgumentException("错误的 $value 类型") } return enum } fun > toJson(value: T) = value.name.uppercase() } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/FormatType.kt ================================================ package com.ke.hs_tracker.module.entity import androidx.annotation.StringRes import com.ke.hs_tracker.module.R enum class FormatType(@StringRes val title: Int) { Unknown(R.string.module_unknown), /** * 标准 */ Standard(R.string.module_standard), /** * 狂野 */ Wild(R.string.module_wild), /** * 经典 */ Classic(R.string.module_classic) } val String.toFormatType: FormatType get() = FormatType.values().find { this.contains(it.name, true) } ?: FormatType.Unknown ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/GameCardType.kt ================================================ package com.ke.hs_tracker.module.entity /** * 卡牌类型 */ enum class GameCardType { Game, /** * 玩家 */ Player, /** * 英雄 */ Hero, /** * 英雄技能 */ HeroPower, /** * 牌库中的牌的状态 */ Invalid, /** * 随从身上的buff或战场上的buff(例如下一张法强怪法力值减少1) */ Enchantment, /** * 法术 */ Spell, /** * 随从 */ Minion } /** * 字符串转 CardType类型 */ internal fun String.toCardType(fallback: GameCardType = GameCardType.Game): GameCardType { return GameCardType.values().find { it.name.equals(this.replace("_", ""), true) } ?: fallback } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/GameEvent.kt ================================================ package com.ke.hs_tracker.module.entity import com.ke.hs_tracker.module.db.Game /** * 游戏事件 */ sealed interface GameEvent { /** * 游戏开始 */ object OnGameStart : GameEvent /** * 游戏结束 */ data class OnGameOver( val game: Game ) : GameEvent /** * 插入一张卡牌到用户的牌库 */ data class InsertCardToUserDeck( val cardId: String ) : GameEvent /** * 从牌库中移除一张卡牌 */ data class RemoveCardFromUserDeck( val cardId: String ) : GameEvent /** * 插入一张卡牌到墓地 */ data class InsertCardToGraveyard(val cardId: String, val isUser: Boolean) : GameEvent } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/GameType.kt ================================================ package com.ke.hs_tracker.module.entity import androidx.annotation.StringRes import com.ke.hs_tracker.module.R enum class GameType(@StringRes val title: Int) { /** * 排名 */ Ranked(R.string.module_ranked), /** * 休闲 */ Casual(R.string.module_casual), Unknown(R.string.module_unknown) } val String.toGameType: GameType get() = GameType.values().find { this.contains(it.name, true) } ?: GameType.Unknown ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/GraveyardCard.kt ================================================ package com.ke.hs_tracker.module.entity data class GraveyardCard( val card: Card, /** * 插入时间 */ val time: Long = System.currentTimeMillis() ) ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/InsertStackResult.kt ================================================ package com.ke.hs_tracker.module.entity sealed interface InsertStackResult { /** * 插入成功 */ object Success : InsertStackResult /** * 不能插入,例如不在Block内的TAG_CHANGE */ object CanNotInsert : InsertStackResult /** * 结束了 * @param powerTag tag * @param handled 是否处理了本次log日志,例如 FULL_ENTITY - Updating 跟着一个 FULL_ENTITY - Updating的情况,handled就是true,表示已经处理了,不需要在进行处理;如果是FULL_ENTITY - Updating * 跟着一个 TAG_CHANGE,就表示没有处理,需要调用这个方法的自行处理log数据 */ data class Over(val powerTag: PowerTag, val handled: Boolean) : InsertStackResult } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/LogType.kt ================================================ package com.ke.hs_tracker.module.entity enum class LogType(val replace: String) { PowerTaskList("PowerTaskList.DebugPrintPower() -"), GameState("GameState.DebugPrintGame() -") } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Mechanics.kt ================================================ package com.ke.hs_tracker.module.entity import com.squareup.moshi.FromJson import com.squareup.moshi.ToJson /** * 类型 */ enum class Mechanics { /** * 注能 */ Infuse, /** * 过载 */ Overload, /** * 战吼 */ BattleCry, /** * 光环 */ Aura, /** * 冲锋 */ Charge, /** * 嘲讽 */ Taunt, /** * 突袭 */ Rush, /** * 巨型 */ Colossal, /** * 探底 */ Dredge, /** * 潜行 */ Stealth, /** * 圣盾 */ DivineShield, /** * 法强 */ SpellPower, /** * 抽到的时候 */ TopDeck, /** * 亡语 */ DeathRattle, /** * 秘密选择 */ Counter, InvisibleDeathRattle, /** * 变形 */ Morph, /** * 激怒 */ Enraged, /** * 50%几率攻击错误的敌人 */ Forgetful, /** * 本回合生效 */ TagOneTurnEffect, /** * 抉择 */ ChooseOne, /** * 荣誉击杀 */ HonorableKill, /** * 法力迸发 */ SpellBurst, /** * 腐蚀 */ Corrupt, /** * 暴怒 */ Frenzy, /** * 发现 */ Discover, /** * 冻结 */ Freeze, /** * 连击 */ Combo, /** * 无法攻击 */ CantAttack, /** * 触发 */ TriggerVisual, /** * 奥秘 */ Secret, /** * 任务 */ Quest, /** * 克苏恩 */ Ritual, /** * 交易 */ Tradeable, /** * 激励 */ Inspire, /** * 沉默 */ Silence, /** * 风怒 */ Windfury, /** * 回响 */ Echo, /** * 污手党 */ GrimyGoons, /** * 暗金教 */ Kabal, /** * 玉莲帮 */ JadeLotus, /** * 青玉魔像 */ JadeGolem, /** * 不可见的衍生牌 */ EnchantmentInvisible, /** * 英雄技能造成额外的伤害 */ HeroPowerDamage, /** * 回合结束时如果这张牌仍在手牌中,将其摧毁 */ Ghostly, /** * 受到法强翻倍 */ ReceivesDoubleSpellDamageBonus, /** * 零件 */ SparePart, AutoAttack, /** * 唤尸者专属 */ DeathKnight, /** * 偶数 */ CollectionmanagerFilterManaEven, /** * 奇数 */ CollectionmanagerFilterManaOdd, /** * 不受法强影响的法术 */ ImmuneToSpellPower, /** * 无法被沉默 */ CantBeSilenced, CantBeDestroyed, /** * 黑棋国王 */ CantBeFatigued, /** * 支线任务 */ SideQuest, /** * 双生法术 */ TwinSpell, /** * 剧毒 */ Poisonous, /** * 吸血 */ LifeSteal, /** * 流放 */ Outcast, /** * 不可接触的 */ Untouchable, /** * 复仇 */ Avenge, /** * 超杀 */ Overkill, /** * 复生 */ Reborn, AIMustPlay, /** * 免疫 */ Immune, /** * 无法成为法术的目标 */ CantBeTargetedBySpells, /** * 无法成为英雄技能的目标 */ CantBeTargetedByHeroPowers, /** * 磁力 */ Modular, /** * 如果这张牌在你的手牌中,在你的回合开始时,你的英雄受到2点伤害 */ EvilGlow, /** * 相邻的随从获得buff */ AdjacentBuff, /** * 对局开始时 */ StartOfGame, /** * 在你攻击一个随从后,迫使其攻击相邻的一个随从 */ FinishAttackSpellOnDamage, AppearFunctionallyDead, Gears, Puzzle, MultiplyBuffValue, AffectedBySpellPower, DungeonPassiveBuff, IgnoreHideStatsForBigCard, Summoned, /** * 法力渴求 */ ManaThirst, OVERHEAL, VENOMOUS, MAGNETIC, FORGE, TITAN } class MechanicsAdapter { @FromJson fun fromJson(value: String): Mechanics { return EnumMoshiAdapter.fromJson(value, Mechanics.values()) } @ToJson fun toJson(mechanics: Mechanics) = EnumMoshiAdapter.toJson(mechanics) } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/NestedTag.kt ================================================ package com.ke.hs_tracker.module.entity internal sealed interface NestedTag { object CreateGame : NestedTag data class GameEntity(val id: Int) : NestedTag data class Tag(val key: String, val value: String) : NestedTag { fun toPair(): Pair = key to value } data class Player(val entityId: Int, val playerId: Int) : NestedTag //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] CardID= //FULL_ENTITY - Updating [entityName=全副武装! id=65 zone=PLAY zonePos=0 cardId=HERO_01bp player=1] CardID=HERO_01bp data class FullEntity( val entity: Entity, val cardId: String? ) : NestedTag data class Block( val blockType: BlockType, val entity: Entity, val target: Entity? ) : NestedTag data class TagChange( val entity: Entity, val tag: String, val value: String ) : NestedTag { fun convert(): PowerTag.PowerTaskList.TagChange { return PowerTag.PowerTaskList.TagChange(entity, tag, value) } } data class ShowEntity( val entity: Entity, val cardId: String ) : NestedTag object BlockEnd : NestedTag } //internal fun NestedTag.FullEntity.toUpdating(): PowerTag.PowerTaskList.FullEntity.Updating { // return PowerTag.PowerTaskList.FullEntity.Updating( // entityName, cardType, id, zone, zonePosition, cardId, player // ) //} ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/PowerTag.kt ================================================ package com.ke.hs_tracker.module.entity import com.ke.hs_tracker.module.db.ZonePositionChangedEvent sealed interface PowerTag { sealed interface PowerTaskList : PowerTag { /** * 创建游戏 */ //D 19:55:18.1257030 GameState.DebugPrintPower() - CREATE_GAME //D 19:55:18.1257030 GameState.DebugPrintPower() - GameEntity EntityID=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=GAME //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=937 value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=GAME_SEED value=1950487951 //D 19:55:18.1257030 GameState.DebugPrintPower() - Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280] //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CONTROLLER value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=PLAYER //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=PLAYER_ID value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=HERO_ENTITY value=64 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXHANDSIZE value=10 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=STARTHANDSIZE value=4 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=TEAM_ID value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXRESOURCES value=10 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=AVRANK value=336 //D 19:55:18.1257030 GameState.DebugPrintPower() - Player EntityID=3 PlayerID=2 GameAccountId=[hi=144115211015832391 lo=44511141] //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CONTROLLER value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=PLAYER //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=PLAYER_ID value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=HERO_ENTITY value=66 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXHANDSIZE value=10 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=STARTHANDSIZE value=4 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=TEAM_ID value=2 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=3 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXRESOURCES value=10 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1 //D 19:55:18.1257030 GameState.DebugPrintPower() - tag=AVRANK value=338 data class CreateGame( val gameEntity: GameEntity, val player1: Player, val player2: Player ) : PowerTaskList data class TagChange( override val entity: Entity, val tag: String, val value: String, ) : PowerTaskList, ZoneUpdatable { /** * 是否是游戏完成的标志 */ val isGameComplete: Boolean = entity.entityName == "GameEntity" && tag.equals("state", true) && value.equals( "COMPLETE", true ) /** * 是否是玩家胜利或失败 */ val isPlayerWonOrLost: Pair? get() { return if (tag == "PLAYSTATE" && value == "WON") { entity.entityName to true } else if (tag == "PLAYSTATE" && value == "LOST") { entity.entityName to false } else { null } } //TAG_CHANGE Entity=GameEntity tag=TURN value=1 fun isTurnChanged(): Int? { if (entity.isGameEntity && tag == "TURN") { return value.toIntOrNull() } return null } //TAG_CHANGE Entity=GameEntity tag=NUM_TURNS_IN_PLAY value=5 fun isNumTurnsInPlayChanged(): Int? { if (entity.isGameEntity && tag == "NUM_TURNS_IN_PLAY") { return value.toIntOrNull() } return null } override fun getZoneString(): String? { return if (tag == "ZONE") value else null } override fun getZonePositionString(): String? { return if (tag == "ZONE_POSITION") value else null } } data class FullEntity( override val entity: Entity, val payloads: MutableMap = mutableMapOf(), val cardId: String? ) : PowerTaskList, ZoneUpdatable { fun append(value: Pair) { payloads[value.first] = value.second } /** * 是否是更新英雄置于战场 */ fun isUpdateHero(): Pair? { if (entity.zone == Zone.Play && payloads.count { it.key == "CARDTYPE" && it.value == "HERO" } == 1) { return entity.player to entity.cardId } return null } //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=50 zone=DECK zonePos=0 cardId= player=2] CardID= // tag=ZONE value=DECK // tag=CONTROLLER value=2 // tag=ENTITY_ID value=50 override fun getZoneString(): String? { return payloads["ZONE"] } override fun getZonePositionString(): String? { return payloads["ZONE_POSITION"] } override fun convertEntity(entity: Entity): Entity { entity.run { return Entity( entityName, gameCardType, id, zone, zonePosition, this@FullEntity.cardId, player ) } } //起始发牌 //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=49 zone=DECK zonePos=0 cardId= player=2] CardID= // tag=ZONE value=DECK // tag=CONTROLLER value=2 // tag=ENTITY_ID value=49 //发现一张牌 //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=113 zone=SETASIDE zonePos=0 cardId= player=2] CardID= // tag=ZONE value=SETASIDE // tag=CONTROLLER value=2 // tag=ENTITY_ID value=113 // override fun shouldIgnoreSameZone(): Boolean { // return true // } } data class ShowEntity( override val entity: Entity, val cardId: String, val payloads: MutableMap = mutableMapOf() ) : PowerTaskList, ZoneUpdatable { fun append(value: Pair) { payloads[value.first] = value.second } fun entityWithCardId(): Entity { return entity.run { Entity( entityName, gameCardType, id, zone, zonePosition, this@ShowEntity.cardId, player ) } } override fun getZoneString(): String? { return payloads["ZONE"] } override fun getZonePositionString(): String? { return payloads["ZONE_POSITION"] } override fun convertEntity(entity: Entity): Entity { entity.run { return Entity( entityName, gameCardType, id, zone, zonePosition, this@ShowEntity.cardId, player ) } } // override fun isUpdated(): Pair? { // val newZoneString = payloads["ZONE"] ?: return null // val newZone = newZoneString.toZone(Zone.Unknown) // if (newZone == Zone.Unknown) { // throw RuntimeException("错误的zone $newZoneString") // } // if (entity.zone == newZone) { // return null // } // return entity to newZone // } } data class Block( val type: BlockType, val entity: Entity, val target: Entity?, val list: List ) : PowerTaskList { /** * 是否是第一回合 */ fun ifFirstTurn(): Boolean { return type == BlockType.Trigger && entity.isGameEntity && list.mapNotNull { it as? TagChange }.any { it.tag == "FIRST_PLAYER" && it.value == "1" } } private fun isTurnChanged(): Int? { return list.mapNotNull { it as? TagChange }.find { it.isTurnChanged() != null }?.isTurnChanged() } private fun isNumTurnsInPlayChanged(): Int? { return list.mapNotNull { it as? TagChange }.find { it.isNumTurnsInPlayChanged() != null }?.isNumTurnsInPlayChanged() } private fun flush(): List { val entityWithPayloadList = mutableListOf() list.forEach { when (it) { is ShowEntity -> { val entity = it.entityWithCardId() val entityWithPayload = EntityWithPayload(entity) // entityWithPayload.payload.addAll(it.payloads) it.payloads.forEach { entry -> entityWithPayload.add(entry.toPair()) } entityWithPayloadList.add(entityWithPayload) } is TagChange -> { findEntityFromList(it.entity.id, entityWithPayloadList)?.apply { add(it.tag to it.value) } } else -> { } } } return entityWithPayloadList } private fun findEntityFromList( entityId: Int, list: List ): EntityWithPayload? { return list.find { it.entity.id == entityId } } } } sealed interface GameState : PowerTag { data class BuildNumber(val number: String) : GameState data class GameType(val type: String) : GameState data class FormatType(val type: String) : GameState data class ScenarioID(val id: String) : GameState data class PlayerMapping(val id: Int, val name: String) : GameState { /** * 是否是先手 */ val first: Boolean = id == 1 /** * 是否是当前用户 */ val isUser: Boolean = name != "UNKNOWN HUMAN PLAYER" } } } interface ZoneUpdatable { /** * 是否更新了位置 */ fun isUpdateZone(userPlayerId: Int?): ZonePositionChangedEvent? { val newZoneString = getZoneString() val newZone = newZoneString?.toZone(Zone.Unknown) if (newZone == Zone.Unknown) { throw RuntimeException("错误的zone $newZoneString") } val position = getZonePositionString()?.toIntOrNull() if (newZone == null && position == null) { return null } val entity = convertEntity(entity) return ZonePositionChangedEvent( entityId = entity.id, cardId = entity.cardId, currentZone = entity.zone, isUser = entity.player == userPlayerId, currentPosition = entity.zonePosition, newZone = newZone ?: entity.zone, newPosition = position ?: entity.zonePosition ) } fun getZoneString(): String? fun getZonePositionString(): String? val entity: Entity fun convertEntity(entity: Entity): Entity = entity } //interface ZonePositionUpdatable { // /** // * 是否更新了位置 // */ // fun isUpdatePosition(): Pair? { // // val position = getZonePositionString()?.toIntOrNull() ?: return null // // return entity to position // } // // fun getZonePositionString(): String? // // val entity: Entity // // //} ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Race.kt ================================================ package com.ke.hs_tracker.module.entity import androidx.annotation.StringRes import com.ke.hs_tracker.module.R import com.squareup.moshi.FromJson import com.squareup.moshi.ToJson /** * 随从种族 */ enum class Race(@StringRes val titleRes: Int? = null, val tradition: Boolean = false) { /** * 海盗 */ Pirate(R.string.module_pirate, true), /** * 机械 */ Mechanical(R.string.module_mechanical, true), /** * 龙 */ Dragon(R.string.module_dragon, true), /** * 野兽 */ Beast(R.string.module_beast, true), /** * 鱼人 */ Murloc(R.string.module_murloc, true), /** * 恶魔 */ Demon(R.string.module_demon, true), /** * 图腾 */ Totem(R.string.module_totem, true), /** * 元素 */ Elemental(R.string.module_elemental, true), /** * 娜迦 */ Naga(R.string.module_naga, true), Error, /** * 野猪人 */ Quilboar(R.string.module_quilboar), /** * 全部 */ All(R.string.module_all), /** * 佣兵 */ ORC, Troll, /** * 暗夜精灵 */ NightElf, /** * 食人魔 */ Ogre, /** * 牛头人 */ Tauren, Lock, Human, Bloodelf, OldGod, /** * 亡灵 */ Undead, Gnome, Dwarf, /** * 德莱尼 */ Draenei, Halforc, Centaur, Goblin, Furbolg, Egg, Worgen, Treant, Highelf, } class RaceAdapter { @FromJson fun fromJson(value: String): Race { return EnumMoshiAdapter.fromJson(value, Race.values(), Race.Error) } @ToJson fun toJson(value: Race) = EnumMoshiAdapter.toJson(value) } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Rarity.kt ================================================ package com.ke.hs_tracker.module.entity import androidx.annotation.ColorRes import androidx.annotation.StringRes import com.ke.hs_tracker.module.R import com.squareup.moshi.FromJson import com.squareup.moshi.ToJson enum class Rarity(@StringRes val titleRes: Int, @ColorRes val colorRes: Int) { /** * 免费 */ Free(R.string.module_rarity_free, R.color.module_rarity_common), /** * 普通 */ Common(R.string.module_rarity_common, R.color.module_rarity_common), /** * 稀有 */ Rare(R.string.module_rarity_rare, R.color.module_rarity_rare), /** * 史诗 */ Epic(R.string.module_rarity_epic, R.color.module_rarity_epic), /** * 传说 */ Legendary(R.string.module_rarity_legendary, R.color.module_rarity_legendary) } class RarityAdapter { @FromJson fun fromJson(value: String): Rarity { return EnumMoshiAdapter.fromJson( value, Rarity.values() ) } @ToJson fun toJson(value: Rarity) = EnumMoshiAdapter.toJson(value) } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/SpellSchool.kt ================================================ package com.ke.hs_tracker.module.entity import androidx.annotation.StringRes import com.ke.hs_tracker.module.R import com.squareup.moshi.FromJson import com.squareup.moshi.ToJson /** * 法术类型 */ enum class SpellSchool(@StringRes val titleRes: Int) { /** * 奥数 */ Arcane(R.string.module_arcane), // /** // * 冰霜 // */ // Freeze, /** * 冰霜 */ Frost(R.string.module_frost), /** * 火焰 */ Fire(R.string.module_fire), /** * 自然 */ Nature(R.string.module_nature), /** * 暗影 */ Shadow(R.string.module_shadow), /** * 神圣 */ Holy(R.string.module_holy), /** * 邪能 */ Fel(R.string.module_Fel), /** * 冲锋攻击 */ PhysicalCombat(R.string.module_all) } class SpellSchoolAdapter { @FromJson fun fromJson(value: String): SpellSchool? { return SpellSchool.values().find { it.name.equals(value.replace("_", ""), true) } } @ToJson fun toJson(spellSchool: SpellSchool?) = spellSchool?.name?.uppercase() } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Turn.kt ================================================ package com.ke.hs_tracker.module.entity sealed interface Turn { /** * 游戏初始化 */ data class InitialGame( val buildNumber: String, val gameType: String, val formatType: String, val scenarioID: Int, val player1: Pair, val player2: Pair, ) /** * 卡牌初始化 */ data class CreateGame( val player1Cards: List, val player2Cards: List, val player1HeroId: String, val player1HeroPowerId: String, val player2HeroId: String, val player2HeroPowerId: String ) : Turn /** * 确定手牌回合 */ data class First( val firstPlayerName: String, val player1Cards: List, val player2Cards: List ) : Turn } data class PlayerInitialCard( val playerId: Int, val position: Int, val entityId: Int, //自己的卡牌才能看到id val cardId: String? ) ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Zone.kt ================================================ package com.ke.hs_tracker.module.entity enum class Zone { /** * 战场 */ Play, /** * 牌库 */ Deck, /** * 发现的牌的位置 */ SetAside, /** * 墓地 打出的法术牌和死亡的随从会进入 */ Graveyard, /** * 手牌 */ Hand, /** * 位置 */ Unknown, /** * 衍生牌被消耗后会去到这个地方 */ //TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1068 value=5 //TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1068 value=0 //TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=ZONE value=REMOVEDFROMGAME //TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1234 value=94 RemovedFromGame, /** * 奥秘 */ Secret } internal fun String.toZone(fallback: Zone = Zone.Deck): Zone { return Zone.values().firstOrNull { it.name.equals(this, true) } ?: fallback } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/entity/ZoneCard.kt ================================================ package com.ke.hs_tracker.module.entity import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize data class ZoneCard( val card: Card?, val entityId: Int, val position: Int ) : Parcelable ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/parser/BlockTagStack.kt ================================================ package com.ke.hs_tracker.module.parser import com.ke.hs_tracker.module.entity.* import com.orhanobut.logger.Logger import java.util.* import javax.inject.Inject interface BlockTagStack { /** * 插入一条日志 */ fun insert(line: String): InsertStackResult } class BlockTagStackImpl @Inject constructor() : BlockTagStack { private val nestedTagList = LinkedList() override fun insert(line: String): InsertStackResult { //CREATE_GAME var matchResult = PowerParserImpl.CREATE_GAME_PATTERN.matchEntire(line) if (matchResult != null) { //游戏开始 nestedTagList.clear() nestedTagList.add(NestedTag.CreateGame) return InsertStackResult.Success } //GameEntity EntityID=1 matchResult = PowerParserImpl.GAME_ENTITY_PATTERN.matchEntire(line) if (matchResult != null) { val entityId = matchResult.groupValues[1].toInt() nestedTagList.add(NestedTag.GameEntity(entityId)) return InsertStackResult.Success } //Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280] matchResult = PowerParserImpl.PLAYER_ENTITY_PATTERN.matchEntire(line) if (matchResult != null) { val entityId = matchResult.groupValues[1].toInt() val playerId = matchResult.groupValues[2].toInt() nestedTagList.add(NestedTag.Player(entityId, playerId)) return InsertStackResult.Success } //tag=CARDTYPE value=GAME matchResult = PowerParserImpl.TAG_PATTERN.matchEntire(line) if (matchResult != null) { val key = matchResult.groupValues[1] val value = matchResult.groupValues[2] nestedTagList.add(NestedTag.Tag(key, value)) return InsertStackResult.Success } //BLOCK_START // BlockType=TRIGGER // Entity=GameEntity // EffectCardId=System.Collections.Generic.List`1[System.String] // EffectIndex=-1 // Target=0 // SubOption=-1 // TriggerKeyword=TAG_NOT_SET matchResult = PowerParserImpl.BLOCK_START_PATTERN.matchEntire(line) if (matchResult != null) { val blockType = matchResult.groupValues[1].toBlockType() val entity = Entity.createFromContent(matchResult.groupValues[2])!! val target = Entity.createFromContent(matchResult.groupValues[5]) val block = NestedTag.Block(blockType, entity, target) nestedTagList.add(block) return InsertStackResult.Success } matchResult = PowerParserImpl.BLOCK_END_PATTERN.matchEntire(line) if (matchResult != null) { //块结束了 if (nestedTagList.isEmpty()) { Logger.d("准备插入一个end到空的列表里面") return InsertStackResult.Success } nestedTagList.add(NestedTag.BlockEnd) // val blockStartList = nestedTagList.filterIsInstance() val blockCount = nestedTagList.count { it is NestedTag.Block } val blockEndCount = nestedTagList.count { it is NestedTag.BlockEnd } if (blockCount == blockEndCount) { return InsertStackResult.Over(flushBlock(nestedTagList), true) } else { return InsertStackResult.Success } } //TAG_CHANGE Entity=阿克萌德#51240 tag=CURRENT_PLAYER value=1 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=1 matchResult = PowerParserImpl.TAG_CHANGE_PATTERN.matchEntire(line) if (matchResult != null) { return when (val first = nestedTagList.firstOrNull()) { is NestedTag.FullEntity -> { val powerTag = flushFullEntityWhenFirst() //处理堆栈 InsertStackResult.Over(powerTag, false) } is NestedTag.ShowEntity -> { val showEntity = PowerTag.PowerTaskList.ShowEntity( first.entity, first.cardId ) nestedTagList.forEach { if (it is NestedTag.TagChange) { showEntity.payloads[it.tag] = it.value } } nestedTagList.clear() InsertStackResult.Over(showEntity, false) } is NestedTag.Block -> { val tagChange = NestedTag.TagChange( Entity.createFromContent(matchResult.groupValues[1])!!, matchResult.groupValues[2], matchResult.groupValues[3], ) nestedTagList.add(tagChange) InsertStackResult.Success } else -> { InsertStackResult.CanNotInsert } } } //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1] CardID= matchResult = PowerParserImpl.FULL_ENTITY_PATTERN.matchEntire(line) if (matchResult != null) { val first = nestedTagList.firstOrNull() if (first is NestedTag.CreateGame) { //create game 接 full entity val createGame = createCreateGameTag() val pair = matchResult.groupValues[1] to matchResult.groupValues[2] insertFullEntity(pair) return InsertStackResult.Over(createGame, true) } else if (first is NestedTag.FullEntity) { //连续两个full entity val result = flushFullEntityWhenFirst() insertFullEntity(matchResult.groupValues[1] to matchResult.groupValues[2]) return InsertStackResult.Over(result, true) } insertFullEntity(matchResult.groupValues[1] to matchResult.groupValues[2]) return InsertStackResult.Success } //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=85 zone=SETASIDE zonePos=0 cardId= player=2] CardID=SCH_231e matchResult = PowerParserImpl.SHOW_ENTITY.matchEntire(line) if (matchResult != null) { val first = nestedTagList.firstOrNull() val fullEntity = if (first is NestedTag.FullEntity) { flushFullEntityWhenFirst() } else null val entity = Entity.createFromContent(matchResult.groupValues[1])!! val cardId = matchResult.groupValues[2] nestedTagList.add(NestedTag.ShowEntity(entity, cardId)) return if (fullEntity == null) { InsertStackResult.Success } else { InsertStackResult.Over(fullEntity, true) } } return InsertStackResult.CanNotInsert } /** * 如果栈中的第一个是FullEntity,就开始处理 */ private fun flushFullEntityWhenFirst(): PowerTag.PowerTaskList.FullEntity { val map = mutableMapOf() val first = nestedTagList.removeFirst() as NestedTag.FullEntity nestedTagList.map { val result = it as? NestedTag.Tag ?: throw IllegalArgumentException("错误的类型,应该是Tag 但现在是 $it") result }.forEach { map[it.key] = it.value } //清空栈 nestedTagList.clear() return PowerTag.PowerTaskList.FullEntity( first.entity, map, first.cardId ) // return when { // isInsertCardToDeck(first) -> { // //插入一张卡牌到牌库 // flushFullEntityInsertCardToDeck() // } // isInsertHeroToPlay(first) -> { // //放置英雄牌到战场 // flushFullEntityInsertHeroToPlay() // } // isInsertHeroPowerToPlay(first) -> { // //放置英雄技能到战场 // flushFullEntityInsertHeroPowerToPlay() // } // else -> throw RuntimeException("无法处理的 full entity $first") // } } // /** // * 是否是置入英雄技能到战场 // */ // private fun isInsertHeroPowerToPlay(fullEntity: NestedTag.FullEntity): Boolean { // if (fullEntity.entity.zone != Zone.Play) { // return false // } // // nestedTagList.forEach { // if (it is NestedTag.Tag && "cardType".equals( // it.key, // true // ) && GameCardType.HeroPower.name.equals( // it.value.replace("_", ""), // true // ) // ) { // return true // } // } // // // return false // } // private fun flushFullEntityInsertHeroPowerToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay { // var last = nestedTagList.removeLastOrNull() // //移除第一个 // val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity // val map = mutableMapOf() // while (last != null) { // when (last) { // is NestedTag.Tag -> { // map[last.key] = last.value // } // else -> { // throw RuntimeException("last的类型必须是Tag 但现在是 $last") // } // } // last = nestedTagList.removeLastOrNull() // } // return PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay.createFromEntityAndMap( // entity, // map // ) // } // // private fun flushFullEntityInsertHeroToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay { // var last = nestedTagList.removeLastOrNull() // //移除第一个 // val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity // val map = mutableMapOf() // while (last != null) { // when (last) { // is NestedTag.Tag -> { // map[last.key] = last.value // } // else -> { // throw RuntimeException("last的类型必须是Tag 但现在是 $last") // } // } // last = nestedTagList.removeLastOrNull() // } // return PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay.createFromEntityAndMap( // entity, // map // ) // } // /** // * 是否是置入英雄卡到战场 // */ // private fun isInsertHeroToPlay(fullEntity: NestedTag.FullEntity): Boolean { // if (fullEntity.entity.zone != Zone.Play) { // return false // } // // nestedTagList.forEach { // if (it is NestedTag.Tag && "cardType".equals( // it.key, // true // ) && GameCardType.Hero.name.equals( // it.value, // true // ) // ) { // return true // } // } // // // return false // } // /** // * 是否是置入英雄卡到战场 // */ // private fun isInsertCardToDeck(fullEntity: NestedTag.FullEntity): Boolean { // // return fullEntity.entity.zone == Zone.Deck && fullEntity.entity.gameCardType == GameCardType.Invalid // } // private fun flushFullEntityInsertCardToDeck(): PowerTag.PowerTaskList.FullEntity.InsertToDeck { // var last = nestedTagList.removeLastOrNull() // //移除第一个 // val fullEntity = nestedTagList.removeFirst() as NestedTag.FullEntity // val map = mutableMapOf() // while (last != null) { // when (last) { // is NestedTag.Tag -> { // map[last.key] = last.value // } // else -> { // throw RuntimeException("last的类型必须是Tag 但现在是 $last") // } // } // last = nestedTagList.removeLastOrNull() // } // // if (map.size != 3) throw RuntimeException("在插入卡牌到牌库的情况下,tag数量必须是3个") // // return PowerTag.PowerTaskList.FullEntity.InsertToDeck.createFromEntityAndMap( // fullEntity.entity, // map // ) // // } private fun insertFullEntity(pair: Pair) { val fullEntity = createFullEntityByContent(pair.first, pair.second.ifEmpty { null }) nestedTagList.add(fullEntity) } /** * 根据字符串创建FullEntity */ //[entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1] //[entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1] private fun createFullEntityByContent(content: String, cardId: String?): NestedTag.FullEntity { val entity: Entity = Entity.createFromContent(content)!! return NestedTag.FullEntity(entity, cardId) } private fun createCreateGameTag(): PowerTag.PowerTaskList.CreateGame { val first = nestedTagList.removeFirstOrNull() if (first == NestedTag.CreateGame) { val keyValueMap = mutableMapOf() var last = nestedTagList.removeLastOrNull() var player1: Player? = null var player2: Player? = null while (last != null) { when (last) { is NestedTag.GameEntity -> { val game = GameEntity( GameCardType.Game, last.id ) return PowerTag.PowerTaskList.CreateGame( game, player1!!, player2!! ) } is NestedTag.Player -> { keyValueMap["playerid"] = last.playerId.toString() keyValueMap["entityid"] = last.entityId.toString() if (player2 == null) { player2 = Player.fromMap(keyValueMap) } else { player1 = Player.fromMap(keyValueMap) } keyValueMap.clear() } is NestedTag.Tag -> { keyValueMap[last.key.replace("_", "").lowercase()] = last.value } else -> throw IllegalArgumentException("非法状态错误 $last") } last = nestedTagList.removeLastOrNull() } } else { throw IllegalArgumentException("第一个必须是 CreateGame,但现在是 $first") } throw RuntimeException("无法创建CreateGame") } private fun flushBlock( source: MutableList, ): PowerTag.PowerTaskList.Block { //有可能出现多级嵌套 val header = source.removeFirst() val first = header as? NestedTag.Block if (first == null) { throw RuntimeException("列表的第一个应该是Block,但现在是不是 现在是 $header,当前列表为 $source") } source.removeLast() val payloads = mutableListOf() val tempList = mutableListOf() source.forEachIndexed { index, nestedTag -> when (nestedTag) { is NestedTag.Block -> { // if (tempList.isEmpty()) { tempList.add(nestedTag) // } else { // val blockStartCount = tempList.count { // it is NestedTag.Block // } // if (blockStartCount != 1) { // throw IllegalArgumentException("错误的block数量 $blockStartCount") // } // // val pairedBlockEndIndex = findPairBlockEndIndex(source, index) // if (pairedBlockEndIndex == -1) { // throw IllegalArgumentException("找不到配对的结束标识") // } // val innerBlockList = source.subList(index, pairedBlockEndIndex) // tempList.add(flushBlock(innerBlockList)) // } } is NestedTag.FullEntity -> { if (tempList.isNotEmpty()) { tempList.add(nestedTag) } else { payloads.add( PowerTag.PowerTaskList.FullEntity( nestedTag.entity, mutableMapOf(), nestedTag.cardId ) ) } } is NestedTag.ShowEntity -> { if (tempList.isNotEmpty()) { tempList.add(nestedTag) } else { payloads.add( PowerTag.PowerTaskList.ShowEntity( nestedTag.entity, nestedTag.cardId ) ) } } is NestedTag.Tag -> { if (tempList.isNotEmpty()) { tempList.add(nestedTag) } else { val last = payloads.last() if (last is PowerTag.PowerTaskList.FullEntity) { last.append(nestedTag.toPair()) } else if (last is PowerTag.PowerTaskList.ShowEntity) { last.append(nestedTag.toPair()) } } } is NestedTag.TagChange -> { if (tempList.isNotEmpty()) { tempList.add(nestedTag) } else { payloads.add(nestedTag.convert()) } } NestedTag.BlockEnd -> { tempList.add(NestedTag.BlockEnd) val blockStartCount = tempList.count { it is NestedTag.Block } val blockEndCount = tempList.count { it is NestedTag.BlockEnd } if (blockStartCount == blockEndCount) { payloads.add(flushBlock(tempList)) tempList.clear() } } else -> { throw IllegalArgumentException("非法的数据 $nestedTag") } } } source.clear() return PowerTag.PowerTaskList.Block( first.blockType, first.entity, first.target, payloads ) } } private fun findPairBlockEndIndex(source: List, start: Int): Int { var blockStartCount = 0 source.subList(start, source.size).forEachIndexed { index, it -> if (it is NestedTag.Block) { blockStartCount++ } else if (it is NestedTag.BlockEnd) { if (blockStartCount == 0) { return index } blockStartCount-- } } return -1 } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/parser/DeckCardObserver.kt ================================================ package com.ke.hs_tracker.module.parser import android.content.Context import androidx.documentfile.provider.DocumentFile import com.ke.hs_tracker.module.db.GameDao import com.ke.hs_tracker.module.domain.GetAllCardUseCase import com.ke.hs_tracker.module.domain.GetRealLogDirUseCase import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase import com.ke.hs_tracker.module.entity.* import com.ke.hs_tracker.module.log import com.ke.hs_tracker.module.ui.main.powerFileName import com.ke.mvvm.base.data.successOr import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import java.io.InputStream import javax.inject.Inject /** * 剩余卡牌监听器 */ interface DeckCardObserver { /** * 牌库的卡牌 */ val deckCardList: StateFlow> /** * 自己的墓地 */ val userGraveyardCardList: StateFlow> /** * 对手的墓地 */ val opponentGraveyardCardList: StateFlow> /** * 初始化 */ fun init(scope: CoroutineScope) } class DeckCardObserverImpl @Inject constructor( private val powerParser: PowerParser, private val powerTagHandler: PowerTagHandler, private val getAllCardUseCase: GetAllCardUseCase, private val parseDeckCodeUseCase: ParseDeckCodeUseCase, private val getLogDirUseCase: GetRealLogDirUseCase, private val gameDao: GameDao, @ApplicationContext private val context: Context ) : DeckCardObserver { // private val _userGraveyardCardList = MutableStateFlow>(emptyList()) // // override val userGraveyardCardList: StateFlow> // get() = _userGraveyardCardList /** * 当前用户的卡组 */ private var currentUserDeck: CurrentDeck? = null private val _deckCardList = MutableStateFlow>(emptyList()) // Channel>(capacity = Channel.CONFLATED) override val deckCardList: StateFlow> get() = _deckCardList private val _userGraveyardCardList = MutableStateFlow>(emptyList()) override val userGraveyardCardList: StateFlow> get() = _userGraveyardCardList private val _opponentGraveyardCardList = MutableStateFlow>(emptyList()) override val opponentGraveyardCardList: StateFlow> get() = _opponentGraveyardCardList /** * 所有卡牌 */ private var allCards = emptyList() /** * 当前卡组的卡牌 */ private var currentDeckList: List = emptyList() /** * 当前卡组剩余的卡牌 */ private var deckLeftCardList: List = listOf() /** * 获取炉石log文件夹 */ private suspend fun getLogsDir(): DocumentFile? { return getLogDirUseCase(Unit).successOr(null) } /** * 获取文件流 */ private suspend fun getFileStream(fileName: String): InputStream? = withContext(Dispatchers.IO) { val documentFile = getLogsDir()?.findFile(fileName) if (documentFile == null) { "无法访问 $fileName 文件".log() return@withContext null } context.contentResolver.openInputStream(documentFile.uri) } override fun init( scope: CoroutineScope, ) { val interval = 1500L scope.launch { clearPowerLogFile() } scope.launch { ///获取所有卡牌 allCards = getAllCardUseCase(Unit).successOr(emptyList()) } val deckFileObserver = DeckFileObserver(interval) { getFileStream("Decks.log") } val powerFileObserver = PowerFileObserver(interval) { getFileStream(powerFileName) }.apply { } scope.launch { //监听牌库 delay(1000) deckFileObserver .start() .flowOn(Dispatchers.IO) .map { currentUserDeck = it parseDeckCodeUseCase(it.code).successOr(emptyList()) }.collect { currentDeckList = it _deckCardList.value = it.toList() // _deckCardList.send(it) } } scope.launch { powerTagHandler.gameEventFlow.collect { when (it) { null -> { } is GameEvent.OnGameOver -> { _userGraveyardCardList.value = emptyList() _opponentGraveyardCardList.value = emptyList() clearPowerLogFile() deckLeftCardList = currentDeckList.toList() _deckCardList.value = deckLeftCardList.toList() // _deckCardList.send(deckLeftCardList) it.game.apply { userDeckCode = currentUserDeck?.code ?: "" userDeckName = currentUserDeck?.name ?: "" scope.launch { gameDao.insert(this@apply) } } powerFileObserver.reset() } GameEvent.OnGameStart -> { _userGraveyardCardList.value = emptyList() _opponentGraveyardCardList.value = emptyList() // deckLeftCardList = currentDeckList // "清空卡牌 OnGameStart ,deckLeftCardList ${deckLeftCardList.size} , currentDeckList ${currentDeckList.size}".log() // deckLeftCardList.clear() // deckLeftCardList.addAll(currentDeckList) deckLeftCardList = currentDeckList.toList() _deckCardList.value = deckLeftCardList.toList() // _deckCardList.send(deckLeftCardList) } is GameEvent.RemoveCardFromUserDeck -> { onUserDeckCardListChanged(it.cardId, true) } is GameEvent.InsertCardToUserDeck -> { onUserDeckCardListChanged(it.cardId, false) } is GameEvent.InsertCardToGraveyard -> { onGraveyardCardsChanged(it.cardId, it.isUser) } } } } powerParser.powerTagListener = { powerTagHandler.handle(it) } scope.launch { powerFileObserver.start() .flowOn(Dispatchers.IO) .collect { list -> list.forEach { powerParser.parse(it) } } } } private fun onGraveyardCardsChanged(cardId: String, isUser: Boolean) { //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=106 zone=PLAY zonePos=0 cardId= player=1] tag=ZONE value=GRAVEYARD //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=5 zone=SECRET zonePos=0 cardId= player=1] tag=COST value=2 //如果对面打出一张奥秘拍 会直接进入墓地 // ?: throw RuntimeException("没有id $entity") val card = allCards.find { it.id == cardId } ?: return if (card.type == CardType.Enchantment) { // "衍生牌 $card 不能放到墓地去".log() return } // "插入一张牌到墓地 $card $entity".log() //TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD if (isUser) { _userGraveyardCardList.value += CardBean(card, 1) } else { _opponentGraveyardCardList.value += CardBean(card, 1) } } /** * 清空log文件 */ private suspend fun clearPowerLogFile() { val documentFile = getLogsDir()?.findFile(powerFileName) documentFile?.apply { context.contentResolver.openOutputStream(uri, "wt")?.use { it.write("".encodeToByteArray()) it.flush() it.close() } } } /** * 用户牌库的卡牌发生了变化 */ private fun onUserDeckCardListChanged(cardId: String, remove: Boolean) { val card = allCards.find { it.id == cardId } ?: throw IllegalArgumentException("找不到id是 $cardId 的卡牌") if (card.type == CardType.Enchantment) { return } // "牌库的卡牌发生了变化 $card $remove ".log() val bean = deckLeftCardList.find { it.card.id == card.id } val list = mutableListOf() list.addAll(deckLeftCardList) if (bean == null) { list.add(CardBean(card, 1)) } else { // bean.count = val newCount = if (remove) bean.count - 1 else bean.count + 1 list[deckLeftCardList.indexOf(bean)] = bean.updateCount(newCount) } // if (bean?.count == 3) { // "插入了3张进去? ".log() // } val newList = list.sortedBy { it.card.cost }.filter { it.count > 0 } // deckLeftCardList.clear() // deckLeftCardList.addAll(newList) deckLeftCardList = newList _deckCardList.value = deckLeftCardList.toList() // _deckCardList.send(deckLeftCardList) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/parser/DeckFileObserver.kt ================================================ package com.ke.hs_tracker.module.parser import android.os.Looper import com.ke.hs_tracker.module.entity.CurrentDeck import com.ke.hs_tracker.module.removeTime import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.io.InputStream /** * deck文件观察者 */ class DeckFileObserver constructor( private val interval: Long = 2000, private val deckFileInputStreamProvider: suspend () -> InputStream?, ) { private var oldLogSize = 0L fun reset() { oldLogSize = 0 } suspend fun start(): Flow = flow { if (Thread.currentThread() == Looper.getMainLooper().thread) { throw RuntimeException("不能运行在主线程") } while (true) { deckFileInputStreamProvider()?.reader()?.apply { if (oldLogSize > 0) { skip(oldLogSize) } val text = readText() oldLogSize += text.length val lines = text.lines() .filter { it.isNotEmpty() } listToDeck(lines)?.apply { emit(this) } close() } delay(interval) } } private fun listToDeck(list: List): CurrentDeck? { if (list.isEmpty()) { return null } val contentList = list.map { it.removeTime().third } val name = contentList.findLast { it.startsWith("###", true) } ?: return null val target = contentList.subList(contentList.indexOf(name), contentList.size).toMutableList() target.removeFirst() target.removeFirst() val code = target.removeFirst() return CurrentDeck( name.replace("### ", ""), code ) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/parser/PowerFileObserver.kt ================================================ package com.ke.hs_tracker.module.parser import android.os.Looper import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.io.InputStream /** * power文件观察者 */ class PowerFileObserver( private val interval: Long = 2000, private val fileInputStreamProvider: suspend () -> InputStream?, ) { private var oldLogSize = 0L fun reset() { oldLogSize = 0 } suspend fun start(): Flow> = flow { if (Thread.currentThread() == Looper.getMainLooper().thread) { throw RuntimeException("不能运行在主线程") } delay(interval) while (true) { fileInputStreamProvider() ?.reader() ?.apply { if (oldLogSize > 0) { skip(oldLogSize) } val text = readText() // try { // readText() // } catch (error: Throwable) { // if (BuildConfig.DEBUG) { // error.printStackTrace() // } // val size = fileInputStreamProvider()?.available() ?: 0 // Logger.d("内存溢出了 ,文件大小 $size,当前oldLogSize = $oldLogSize") // oldLogSize += size // // readLines() // "" // } // readLines() // readTextFromFile() oldLogSize += text.length val lines = text.lines() .filter { it.startsWith("D", true) } emit(lines) close() } delay(interval) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/parser/PowerParser.kt ================================================ package com.ke.hs_tracker.module.parser import com.ke.hs_tracker.module.entity.Entity import com.ke.hs_tracker.module.entity.InsertStackResult import com.ke.hs_tracker.module.entity.LogType import com.ke.hs_tracker.module.entity.PowerTag import javax.inject.Inject /** * 日志解析 */ interface PowerParser { /** * 解析一行日志 */ fun parse(content: String) /** * 解析结果监听 */ var powerTagListener: ((PowerTag) -> Unit)? } class PowerParserImpl @Inject constructor( private val blockTagStack: BlockTagStack ) : PowerParser { // private val blockTagStack: BlockTagStack = BlockTagStackImpl() override var powerTagListener: ((PowerTag) -> Unit)? = null override fun parse(content: String) { val pair = checkTypeAndReturnContent(content) ?: return if (pair.first == LogType.PowerTaskList) { handlePowerTaskListLog(pair.second) } else { handleGameStateLog(pair.second) } } /** * 检查日志类型并返回去掉时间和日期前缀的内容 */ private fun checkTypeAndReturnContent(content: String): Pair? { if (content.length < TIME_PREFIX_SIZE) { return null } val noTimeContent = content.substring(TIME_PREFIX_SIZE) if (noTimeContent.startsWith(LogType.GameState.replace)) { return LogType.GameState to noTimeContent.replace(LogType.GameState.replace, "").trim() } else if (noTimeContent.startsWith(LogType.PowerTaskList.replace)) { return LogType.PowerTaskList to noTimeContent.replace(LogType.PowerTaskList.replace, "") .trim() } return null } private fun handlePowerTaskListLog(line: String) { when (val result = blockTagStack.insert(line)) { InsertStackResult.CanNotInsert -> { handleUnSupportNestedTag(line) } is InsertStackResult.Over -> { powerTagListener?.invoke(result.powerTag) if (!result.handled) { //需要自己处理 handleUnSupportNestedTag(line) } } InsertStackResult.Success -> { } } } private fun handleUnSupportNestedTag(line: String) { var matchResult = TAG_CHANGE_PATTERN.matchEntire(line) if (matchResult != null) { handleTagChangeLine( matchResult.groupValues[1], matchResult.groupValues[2], matchResult.groupValues[3] ) } } private fun handleTagChangeLine(content: String, tag: String, value: String) { val entity = Entity.createFromContent(content)!! powerTagListener?.invoke(PowerTag.PowerTaskList.TagChange(entity, tag, value)) } private fun handleGameStateLog(content: String) { BUILD_NUMBER_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.BuildNumber( groupValues[1] ) powerTagListener?.invoke(tag) return } GAME_TYPE_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.GameType( groupValues[1] ) powerTagListener?.invoke(tag) return } FORMAT_TYPE_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.FormatType( groupValues[1] ) powerTagListener?.invoke(tag) return } SCENARIO_ID_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.ScenarioID( groupValues[1] ) powerTagListener?.invoke(tag) return } PLAYER_MAPPING_PATTERN.matchEntire(content)?.apply { val tag = PowerTag.GameState.PlayerMapping( groupValues[1].toInt(), groupValues[2] ) powerTagListener?.invoke(tag) return } } companion object { const val TIME_PREFIX_SIZE = 19 //CREATE_GAME internal val CREATE_GAME_PATTERN = Regex("CREATE_GAME") //BuildNumber=127581 internal val BUILD_NUMBER_PATTERN = Regex("BuildNumber=(.*)") //GameType=GT_CASUAL internal val GAME_TYPE_PATTERN = Regex("GameType=(.*)") //FormatType=FT_WILD internal val FORMAT_TYPE_PATTERN = Regex("FormatType=(.*)") //ScenarioID=2 internal val SCENARIO_ID_PATTERN = Regex("ScenarioID=(.*)") //PlayerID=2, PlayerName=阿克萌德#51240 internal val PLAYER_MAPPING_PATTERN = Regex("PlayerID=(.*), PlayerName=(.*)") // tag=CARDTYPE value=GAME internal val TAG_PATTERN = Regex("tag=(.*) value=(.*)") //TAG_CHANGE Entity=GameEntity tag=STATE value=RUNNING internal val TAG_CHANGE_PATTERN = Regex("TAG_CHANGE Entity=(.*) tag=(.*) value=(.*)") //GameEntity EntityID=1 internal val GAME_ENTITY_PATTERN = Regex("GameEntity EntityID=(.*)") //FULL_ENTITY - Updating [entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1] CardID=HERO_01 internal val FULL_ENTITY_PATTERN = Regex("FULL_ENTITY - Updating (.*) CardID=(.*)") //[entityName=UNKNOWN ENTITY [cardType=INVALID] id=83 zone=DECK zonePos=0 cardId= player=2] val FULL_ENTITY_CONTENT1_PATTERN = Regex("\\[entityName=(.*) \\[cardType=(.*)] id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]") val FULL_ENTITY_CONTENT2_PATTERN = Regex("\\[entityName=(.*) id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]") val SHOW_ENTITY = Regex("SHOW_ENTITY - Updating Entity=(.*) CardID=(.*)") //Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280] internal val PLAYER_ENTITY_PATTERN = Regex("Player EntityID=(.*) PlayerID=(.*) GameAccountId=(.*)") //BLOCK_START BlockType=ATTACK Entity=[entityName=瓦丝琪女士 id=87 zone=PLAY zonePos=1 cardId=BT_109 player=2] // EffectCardId=System.Collections.Generic.List`1[System.String] // EffectIndex=0 Target=[entityName=驯化的雷象 id=78 zone=PLAY zonePos=1 cardId=SCH_714 player=1] SubOption=-1 internal val BLOCK_START_PATTERN = Regex("BLOCK_START BlockType=(.*) Entity=(.*) EffectCardId=(.*) EffectIndex=(.*) Target=(.*) SubOption=(.*)") internal val BLOCK_START_CONTINUATION_PATTERN = Regex("(.*) TriggerKeyword=(.*)") //BLOCK_END internal val BLOCK_END_PATTERN = Regex("BLOCK_END") } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/parser/PowerTagHandler.kt ================================================ package com.ke.hs_tracker.module.parser import com.ke.hs_tracker.module.db.Game import com.ke.hs_tracker.module.domain.GetAllCardUseCase import com.ke.hs_tracker.module.entity.* import com.ke.hs_tracker.module.log import com.ke.mvvm.base.data.successOr import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject /** * power tag处理器 */ interface PowerTagHandler { /** * 处理一个事件 */ fun handle(powerTag: PowerTag) /** * 游戏事件流 */ val gameEventFlow: Flow } class PowerTagHandlerImpl @Inject constructor( private val getAllCardUseCase: GetAllCardUseCase ) : PowerTagHandler { lateinit var allCard: List init { GlobalScope.launch { allCard = getAllCardUseCase(Unit).successOr(emptyList()) } } private val _gameEventFlow = // Channel(capacity = Channel.UNLIMITED) MutableStateFlow(null) override val gameEventFlow: Flow get() = _gameEventFlow /** * 当前卡组 */ // var currentDeck: CurrentDeck? = null /** * 玩家 */ private var user: PowerTag.GameState.PlayerMapping? = null /** * 对手 */ private var opponent: PowerTag.GameState.PlayerMapping? = null /** * 当前回合数 */ private var currentTurn = 0 /** * 游戏实体 */ private var game: Game = Game() private val entityIdAndCardIdMap = mutableMapOf() override fun handle(powerTag: PowerTag) { when (powerTag) { is PowerTag.GameState.BuildNumber -> { game = Game(buildNumber = powerTag.number) // game.userDeckName = currentDeck?.name ?: "" // game.userDeckCode = currentDeck?.code ?: "" } is PowerTag.GameState.FormatType -> { game.formatType = powerTag.type.toFormatType } is PowerTag.GameState.GameType -> { game.gameType = powerTag.type.toGameType } is PowerTag.GameState.PlayerMapping -> { if (powerTag.isUser) { game.userName = powerTag.name user = powerTag // game.isUserFirst = tag.first } else { game.opponentName = powerTag.name opponent = powerTag } } is PowerTag.GameState.ScenarioID -> { game.scenarioID = powerTag.id.toInt() } is PowerTag.PowerTaskList.Block -> { powerTag.list.forEach { handle(it) } } is PowerTag.PowerTaskList.CreateGame -> { //初始化卡牌 onGameStarted() // gameEventListener(GameEvent.OnGameStarted) } is PowerTag.PowerTaskList.FullEntity -> { // handleFullEntity(tag) handleFullEntity(powerTag) //FULL_ENTITY - Updating [entityName=萨尔 id=74 zone=PLAY zonePos=0 cardId=HERO_02 player=2] CardID=HERO_02 // tag=CONTROLLER value=2 // tag=CARDTYPE value=HERO // tag=HEALTH value=30 // tag=ZONE value=PLAY // tag=ENTITY_ID value=74 // tag=FACTION value=NEUTRAL // tag=RARITY value=FREE // tag=HERO_POWER value=687 // tag=SPAWN_TIME_COUNT value=1 powerTag.cardId?.apply { entityIdAndCardIdMap[powerTag.entity.id] = this } } is PowerTag.PowerTaskList.ShowEntity -> { handleShowEntity(powerTag) entityIdAndCardIdMap[powerTag.entity.id] = powerTag.cardId } is PowerTag.PowerTaskList.TagChange -> { handleTagChange(powerTag) } } // tag.toString().log() (powerTag as? ZoneUpdatable)?.apply { isUpdateZone(user?.id)?.let { val cardId = it.cardId ?: entityIdAndCardIdMap[it.entityId] // val event = ZonePositionChangedEvent( // entityId = it.first.id, // cardId = it.first.cardId, // isUser = it.first.player == user?.id, // currentZone = it.first.zone, // newZone = it.second, // currentPosition = it.first.zonePosition, // newPosition = it.first.zonePosition // ) // zoneChangedEventList.add(it) if (it.currentZone == Zone.Deck && it.newZone != Zone.Deck && it.isUser) { //从牌库中抽取一张卡 //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=48 zone=DECK zonePos=0 cardId= player=2] // tag=CONTROLLER value=2 // tag=CARDTYPE value=SPELL // tag=TAG_LAST_KNOWN_COST_IN_HAND value=1 // tag=COST value=1 // tag=PREMIUM value=1 // tag=ZONE value=HAND // tag=ENTITY_ID value=48 // tag=ELITE value=1 // tag=CLASS value=SHAMAN // tag=RARITY value=LEGENDARY // tag=478 value=2 // tag=QUEST_PROGRESS_TOTAL value=3 // tag=676 value=1 // tag=839 value=1 // tag=1043 value=1 // tag=1068 value=0 // tag=QUEST_REWARD_DATABASE_ID value=64323 // tag=SPAWN_TIME_COUNT value=1 //或者 探底 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=3 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=0 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1037 value=1 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE value=HAND //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=6 //探底抽上来的卡是没有cardId的 if (cardId != null) removeCardFromDeck(cardId) } else if (it.newZone == Zone.Deck && it.currentZone != Zone.Deck && it.isUser) { //当心探底 //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] CardID=DED_002 // tag=CONTROLLER value=1 // tag=CARDTYPE value=SPELL // tag=TAG_LAST_KNOWN_COST_IN_HAND value=2 // tag=COST value=2 // tag=ZONE value=DECK // tag=ENTITY_ID value=28 // tag=RARITY value=RARE // tag=DISCOVER value=1 // tag=478 value=1 // tag=1043 value=1 // tag=1068 value=0 // tag=USE_DISCOVER_VISUALS value=1 // tag=SPAWN_TIME_COUNT value=1 // tag=SPELL_SCHOOL value=1 // tag=1711 value=1 // tag=MINI_SET value=1 //有牌插入到牌库 //TAG_CHANGE Entity=[entityName=冷风 id=70 zone=HAND zonePos=2 cardId=AV_266 player=2] tag=ZONE value=DECK //会出现id为空的情况 //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=17 zone=DECK zonePos=0 cardId= player=1] CardID= // tag=ZONE value=DECK // tag=CONTROLLER value=1 // tag=ENTITY_ID value=17 if (cardId != null) insertCardToDeck(cardId) } else if (it.currentZone == Zone.Play && it.newZone == Zone.Graveyard) { onGraveyardCardsChanged(cardId, it.isUser) } else if (it.newZone == Zone.Hand || it.currentZone == Zone.Hand) { //手牌 if (!it.isUser) { // handleOpponentHandChanged(it) } } } } } private fun onGraveyardCardsChanged(cardId: String?, user: Boolean) { if (cardId == null) { return } _gameEventFlow.value = GameEvent.InsertCardToGraveyard(cardId, user) } // private fun findCardById(cardId: String) = // allCard.find { it.id == cardId } ?: throw RuntimeException("id为 $cardId 没有这张牌") private fun removeCardFromDeck(cardId: String) { _gameEventFlow.value = (GameEvent.RemoveCardFromUserDeck(cardId)) } private fun insertCardToDeck(cardId: String) { _gameEventFlow.value = (GameEvent.InsertCardToUserDeck(cardId)) } private fun handleTagChange(tagChange: PowerTag.PowerTaskList.TagChange) { tagChange.isTurnChanged()?.apply { currentTurn = this } if (tagChange.entity.player == user?.id && tagChange.entity.zone == Zone.Hand && tagChange.tag == "ZONE" && tagChange.value == "DECK") { //TAG_CHANGE Entity=[entityName=冷风 id=15 zone=HAND zonePos=3 cardId=AV_266 player=1] tag=ZONE value=DECK // insertCardToDeck(tagChange.entity.cardId!!) } else if (tagChange.tag == "ZONE" && tagChange.value == "GRAVEYARD" && tagChange.entity.zone == Zone.Play) { //TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD //随从死亡后进入墓地 //TAG_CHANGE Entity=[entityName=始生研习 id=63 zone=PLAY zonePos=0 cardId=SCH_270 player=2] tag=ZONE value=GRAVEYARD //打出法术 // onGraveyardCardsChanged(tagChange.entity) } else if (tagChange.isGameComplete) { "游戏结束了".log() onGameOver() } val pair = tagChange.isPlayerWonOrLost if (pair != null) { "有玩家胜利或失败了 $pair $game".log() if (pair.first == game.userName) { game.isUserWin = pair.second } else { game.opponentName = pair.first } } if (tagChange.tag == "FIRST_PLAYER" && tagChange.value == "1") { game.isUserFirst = tagChange.entity.entityName == game.userName } } private fun onGameOver() { // _deckLeftCardList.value = deckCardList game.endTime = System.currentTimeMillis() _gameEventFlow.value = GameEvent.OnGameOver(game) } private fun handleShowEntity(showEntity: PowerTag.PowerTaskList.ShowEntity) { if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads["ZONE"].equals( "hand", true ) ) { //起始手牌 //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=15 zone=DECK zonePos=0 cardId= player=1] CardID=AV_266 // tag=CONTROLLER value=1 // tag=CARDTYPE value=SPELL // tag=TAG_LAST_KNOWN_COST_IN_HAND value=1 // tag=COST value=1 // tag=PREMIUM value=1 // tag=ZONE value=HAND // tag=ENTITY_ID value=15 // tag=RARITY value=COMMON // tag=478 value=1 // tag=1043 value=1 // tag=1068 value=0 // tag=SPAWN_TIME_COUNT value=1 // tag=SPELL_SCHOOL value=3 // removeCardFromDeck(showEntity.cardId) } else if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads["ZONE"].equals( "GRAVEYARD", true ) ) { //爆牌 // 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}) // removeCardFromDeck(showEntity.cardId) } } /** * 游戏开始 */ private fun onGameStarted() { "游戏开始了".log() game.startTime = System.currentTimeMillis() currentTurn = 0 _gameEventFlow.value = (GameEvent.OnGameStart) } private fun handleFullEntity(fullEntity: PowerTag.PowerTaskList.FullEntity) { fullEntity.isUpdateHero()?.apply { val cardClass = allCard.find { it.id == second }?.cardClass ?: return "更新英雄 $this $cardClass".log() if (first == user?.id) { game.userHero = cardClass } else if (first == opponent?.id) { game.opponentHero = cardClass } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/service/ItemViewTouchListener.kt ================================================ package com.ke.hs_tracker.module.service import android.view.MotionEvent import android.view.View import android.view.WindowManager class ItemViewTouchListener( private val wl: WindowManager.LayoutParams, private val windowManager: WindowManager, private val rootView: View ) : View.OnTouchListener { private var x = 0 private var y = 0 override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { when (motionEvent.action) { MotionEvent.ACTION_DOWN -> { x = motionEvent.rawX.toInt() y = motionEvent.rawY.toInt() } MotionEvent.ACTION_MOVE -> { val nowX = motionEvent.rawX.toInt() val nowY = motionEvent.rawY.toInt() val movedX = nowX - x val movedY = nowY - y x = nowX y = nowY wl.apply { x += movedX y += movedY } //更新悬浮球控件位置 windowManager.updateViewLayout(rootView, wl) } else -> { } } return false } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/service/ScaleTouchListener.kt ================================================ package com.ke.hs_tracker.module.service import android.view.MotionEvent import android.view.View import android.view.WindowManager class ScaleTouchListener constructor( private val windowManager: WindowManager, private val rootView: View, private val layoutParams: WindowManager.LayoutParams ) : View.OnTouchListener { private var x = 0 private var y = 0 override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { when (motionEvent.action) { MotionEvent.ACTION_DOWN -> { x = motionEvent.rawX.toInt() y = motionEvent.rawY.toInt() } MotionEvent.ACTION_MOVE -> { val nowX = motionEvent.rawX.toInt() val nowY = motionEvent.rawY.toInt() val movedX = nowX - x val movedY = nowY - y x = nowX y = nowY layoutParams.apply { // x += movedX // y += movedY width += movedX height += movedY } //更新悬浮球控件位置 // windowManager.updateViewLayout(rootView, layoutParams) windowManager.updateViewLayout(rootView, layoutParams) } else -> { } } return true } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/service/WindowService.kt ================================================ package com.ke.hs_tracker.module.service import android.content.Intent import android.os.Build import android.os.IBinder import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.WindowManager import android.widget.AdapterView import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleFloatingWindowBinding import com.ke.hs_tracker.module.log import com.ke.hs_tracker.module.parser.DeckCardObserver import com.ke.hs_tracker.module.ui.common.CardAdapter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint class WindowService : LifecycleService() { private val windowManager: WindowManager by lazy { getSystemService(WINDOW_SERVICE) as WindowManager } private val binding: ModuleFloatingWindowBinding by lazy { val layoutInflater = LayoutInflater.from(applicationContext) ModuleFloatingWindowBinding.inflate(layoutInflater) } private val deckAdapter = CardAdapter() private val graveyardAdapter = CardAdapter() private val opponentGraveyardAdapter = CardAdapter() @Inject lateinit var deckCardObserver: DeckCardObserver private var showList = false private fun showView() { val layoutParams = WindowManager.LayoutParams() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE; } layoutParams.width = resources.getDimension(R.dimen.module_floating_window_width).toInt() layoutParams.height = // LayoutParams.WRAP_CONTENT resources.getDimension(R.dimen.module_floating_window_height).toInt() //需要设置 这个 不然空白地方无法点击 layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE // layoutParams.alpha = 0.8f layoutParams.gravity = Gravity.START or Gravity.TOP windowManager.addView(binding.root, layoutParams) binding.recyclerView.adapter = deckAdapter binding.scale.setOnTouchListener( ScaleTouchListener(windowManager, binding.root, layoutParams) ) binding.hide.setOnClickListener { // binding.recyclerView.isVisible = !binding.recyclerView.isVisible layoutParams.height = if (showList) { resources.getDimension(R.dimen.module_floating_window_height).toInt() } else { resources.getDimension(R.dimen.module_floating_window_header_height).toInt() } windowManager.updateViewLayout(binding.root, layoutParams) showList = !showList } binding.close.setOnClickListener { windowManager.removeView(binding.root) stopSelf() } binding.spinner.adapter = ArrayAdapter.createFromResource( applicationContext, R.array.module_spinner, android.R.layout.simple_list_item_1 ) binding.spinner.onItemSelectedListener = object : OnItemSelectedListener { override fun onItemSelected( parent: AdapterView<*>, view: View, position: Int, id: Long ) { val adapter = when (position) { 0 -> { deckAdapter } 1 -> { graveyardAdapter } 2 -> { opponentGraveyardAdapter } else -> throw IllegalArgumentException("错误的position $position") } binding.recyclerView.adapter = adapter windowManager.updateViewLayout(binding.root, layoutParams) } override fun onNothingSelected(parent: AdapterView<*>?) { } } binding.spinner.setSelection(0) binding.root.setOnTouchListener( ItemViewTouchListener( layoutParams, windowManager, binding.root ) ) } override fun onCreate() { super.onCreate() showView() deckCardObserver.init(lifecycleScope) // lifecycleScope.launch { // delay(5000) // throw RuntimeException("测试异常") // } lifecycleScope.launch { deckCardObserver.deckCardList.collect { // adapter.setList(it) deckAdapter.setDiffNewData(it.toMutableList()) } } lifecycleScope.launch { deckCardObserver.userGraveyardCardList.collect { graveyardAdapter.setDiffNewData(it.toMutableList()) } } lifecycleScope.launch { deckCardObserver.opponentGraveyardCardList.collect { opponentGraveyardAdapter.setDiffNewData(it.toMutableList()) } } } override fun onDestroy() { super.onDestroy() "service 挂了".log() } override fun onBind(intent: Intent): IBinder? { return null } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/chart/GetSummaryChartViewDataUseCase.kt ================================================ package com.ke.hs_tracker.module.ui.chart import com.ke.hs_tracker.module.db.GameDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.entity.CardClass import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject internal class GetSummaryChartViewDataUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, private val gameDao: GameDao ) : UseCase(dispatcher) { override suspend fun execute(parameters: Unit): SummaryChartViewData { val games = gameDao.getAll() val winCount = games.count { it.isUserWin == true } val lossCount = games.count { it.isUserWin == false } val firstHandCount = games.count { it.isUserFirst == true } val secondHandCount = games.count { it.isUserFirst == false } val firstHandWinCount = games.count { it.isUserFirst == true && it.isUserWin == true } val firstHandLossCount = games.count { it.isUserFirst == true && it.isUserWin == false } val secondHandWinCount = games.count { it.isUserFirst == false && it.isUserWin == true } val secondHandLossCount = games.count { it.isUserFirst == false && it.isUserWin == false } val classCounts = CardClass.values() .filter { it.isHero }.map { it to games.count { game -> game.opponentHero == it } } return SummaryChartViewData( winCount, lossCount, firstHandCount, secondHandCount, firstHandWinCount, firstHandLossCount, secondHandWinCount, secondHandLossCount, classCounts ) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/chart/PieChartData.kt ================================================ package com.ke.hs_tracker.module.ui.chart import androidx.annotation.ColorRes data class PieChartData( val label: String, val value: Int, @ColorRes val color: Int ) ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/chart/SummaryChartActivity.kt ================================================ package com.ke.hs_tracker.module.ui.chart import android.graphics.Color import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.github.mikephil.charting.charts.PieChart import com.github.mikephil.charting.data.PieData import com.github.mikephil.charting.data.PieDataSet import com.github.mikephil.charting.data.PieEntry import com.github.mikephil.charting.formatter.PercentFormatter import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivitySummaryChartBinding import com.ke.mvvm.base.data.Result import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint class SummaryChartActivity : AppCompatActivity() { @Inject internal lateinit var getSummaryChartViewDataUseCase: GetSummaryChartViewDataUseCase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setContentView(R.layout.module_activity_summary_chart) val binding: ModuleActivitySummaryChartBinding = ModuleActivitySummaryChartBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } // binding.all.apply { // //设置成实心 // holeRadius = 0f // transparentCircleRadius = 0f // //禁用描述 // description.isEnabled = false // //禁用颜色描述 // legend.isEnabled = false // setUsePercentValues(true) // val pieEntryList = mutableListOf() // pieEntryList.add(PieEntry(41f, "胜利41")) // pieEntryList.add(PieEntry(60f, "失败60")) // val pieDataSet = PieDataSet(pieEntryList, "") // pieDataSet.valueTextSize = 20f // pieDataSet.valueTextColor = Color.WHITE // // pieDataSet.colors = listOf(R.color.module_win, R.color.module_loss).map { // resources.getColor(it, null) // } // val pieData = PieData(pieDataSet) // //显示次数 // pieData.setDrawValues(true) // pieData.setValueFormatter(PercentFormatter()) // data = pieData // invalidate() // } lifecycleScope.launch { val result = getSummaryChartViewDataUseCase(Unit) when (result) { is Result.Success -> { setChartData(result.data, binding) } is Result.Error -> { result.exception.printStackTrace() } } } } private fun setChartData( summaryChartViewData: SummaryChartViewData, binding: ModuleActivitySummaryChartBinding ) { setupPieChart( listOf( PieChartData( "${getString(R.string.module_win)}${summaryChartViewData.winCount}", summaryChartViewData.winCount, R.color.module_win ), PieChartData( "${getString(R.string.module_loss)}${summaryChartViewData.lossCount}", summaryChartViewData.lossCount, R.color.module_loss ), ), binding.all ) setupPieChart( listOf( PieChartData( "${getString(R.string.module_first_hand)}${summaryChartViewData.firstHandCount}", summaryChartViewData.firstHandCount, R.color.module_win ), PieChartData( "${getString(R.string.module_second_hand)}${summaryChartViewData.secondHandCount}", summaryChartViewData.secondHandCount, R.color.module_loss ), ), binding.firstHandPercent ) setupPieChart( listOf( PieChartData( "${getString(R.string.module_win)}${summaryChartViewData.firstHandWinCount}", summaryChartViewData.firstHandWinCount, R.color.module_win ), PieChartData( "${getString(R.string.module_loss)}${summaryChartViewData.firstHandLossCount}", summaryChartViewData.firstHandLossCount, R.color.module_loss ), ), binding.firstHandRate ) setupPieChart( listOf( PieChartData( "${getString(R.string.module_win)}${summaryChartViewData.secondHandWinCount}", summaryChartViewData.secondHandWinCount, R.color.module_win ), PieChartData( "${getString(R.string.module_loss)}${summaryChartViewData.secondHandLossCount}", summaryChartViewData.secondHandLossCount, R.color.module_loss ), ), binding.secondHandRate ) setupPieChart( summaryChartViewData.classCounts.map { PieChartData( getString(it.first.titleRes) + it.second, it.second, it.first.color ) }, binding.classDistribution ) } private fun setupPieChart(list: List, pieChart: PieChart) { pieChart.apply { //设置成实心 holeRadius = 0f transparentCircleRadius = 0f //禁用描述 description.isEnabled = false //禁用颜色描述 legend.isEnabled = false setUsePercentValues(true) val pieDataSet = PieDataSet(list.map { return@map PieEntry( it.value.toFloat(), it.label ) }, "") pieDataSet.valueTextSize = 16f pieDataSet.valueTextColor = Color.WHITE pieDataSet.colors = list.map { it.color }.map { resources.getColor(it, null) } val pieData = PieData(pieDataSet) // pieData.setDrawValues(true) pieData.setValueFormatter(PercentFormatter(this)) // pieData.setValueFormatter(object : ValueFormatter() { // override fun getFormattedValue(value: Float): String { // return "$value %" // } // }) data = pieData invalidate() } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/chart/SummaryChartViewData.kt ================================================ package com.ke.hs_tracker.module.ui.chart import com.ke.hs_tracker.module.entity.CardClass internal data class SummaryChartViewData( /** * 总胜利次数 */ val winCount: Int, /** * 总失败次数 */ val lossCount: Int, /** * 先手次数 */ val firstHandCount: Int, /** * 后手次数 */ val secondHandCount: Int, /** * 先手胜利次数 */ val firstHandWinCount: Int, /** * 先手失败次数 */ val firstHandLossCount: Int, /** * 后手胜利次数 */ val secondHandWinCount: Int, /** * 后手失败次数 */ val secondHandLossCount: Int, /** * 职业出现频度 */ val classCounts: List> ) ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleDetailActivity.kt ================================================ package com.ke.hs_tracker.module.ui.classbattledetail import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.DividerItemDecoration import com.ke.hs_tracker.module.databinding.ModuleActivityClassBattleDetailBinding import com.ke.hs_tracker.module.databinding.ModuleItemClassBattleDetailBinding import com.ke.hs_tracker.module.entity.CardClass import com.ke.mvvm.base.data.ViewStatus import com.ke.mvvm.base.ui.BaseViewBindingAdapter import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class ClassBattleDetailActivity : AppCompatActivity() { internal val viewModel: ClassBattleDetailViewModel by viewModels() private val adapter by lazy { object : BaseViewBindingAdapter() { override fun bindItem( item: ClassBattleItem, viewBinding: ModuleItemClassBattleDetailBinding, viewType: Int, position: Int ) { viewBinding.apply { value1.setText(item.hero.titleRes) value2.text = item.times.toString() value3.text = item.win.toString() value4.text = item.loss .toString() value5.text = "${item.rate}%" } } override fun createViewBinding( inflater: LayoutInflater, parent: ViewGroup, viewType: Int ): ModuleItemClassBattleDetailBinding { return ModuleItemClassBattleDetailBinding.inflate(inflater, parent, false) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setContentView(R.layout.module_activity_class_battle_detail) val binding = ModuleActivityClassBattleDetailBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration( DividerItemDecoration( this, DividerItemDecoration.VERTICAL ) ) launchAndRepeatWithViewLifecycle { viewModel.viewStatus.collect { when (it) { is ViewStatus.Loading -> { } is ViewStatus.Content -> { adapter.setList(it.data) } is ViewStatus.Error -> { } } } } } companion object { fun createIntent(context: Context, cardClass: CardClass): Intent { return Intent(context, ClassBattleDetailActivity::class.java).apply { putExtra(EXTRA_CARD_CLASS, cardClass) } } internal const val EXTRA_CARD_CLASS = "extra_card_class" } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleDetailViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.classbattledetail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.entity.CardClass import com.ke.mvvm.base.data.successOr import com.ke.mvvm.base.ui.BaseContentViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel internal class ClassBattleDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val getClassBattleItemListUseCase: GetClassBattleItemListUseCase ) : BaseContentViewModel>() { internal val cardClass: CardClass = savedStateHandle.get(ClassBattleDetailActivity.EXTRA_CARD_CLASS)!! init { viewModelScope.launch { showLoading() showContent(getClassBattleItemListUseCase(cardClass).successOr(emptyList())) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleItem.kt ================================================ package com.ke.hs_tracker.module.ui.classbattledetail import com.ke.hs_tracker.module.entity.CardClass internal data class ClassBattleItem( val hero: CardClass, val times: Int, val win: Int, val loss: Int, val rate: Int ) ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/GetClassBattleItemListUseCase.kt ================================================ package com.ke.hs_tracker.module.ui.classbattledetail import com.ke.hs_tracker.module.db.GameDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.entity.CardClass import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject internal class GetClassBattleItemListUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, private val dao: GameDao ) : UseCase>(dispatcher) { override suspend fun execute(parameters: CardClass): List { val items = mutableListOf() dao.getByHero(parameters) .groupBy { it.opponentHero!! }.forEach { cardClass, list -> val times = list.size val win = list.count { it.isUserWin == true } val loss = times - win val rate = win * 100 / times val item = ClassBattleItem(cardClass, times, win, loss, rate) items.add(item) } return items } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/common/CardAdapter.kt ================================================ package com.ke.hs_tracker.module.ui.common import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import com.ke.hs_tracker.module.bindCard import com.ke.hs_tracker.module.databinding.ModuleItemCardBinding import com.ke.hs_tracker.module.entity.CardBean import com.ke.mvvm.base.ui.BaseViewBindingAdapter class CardAdapter : BaseViewBindingAdapter() { init { setDiffCallback(object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: CardBean, newItem: CardBean): Boolean { return oldItem == newItem } override fun areContentsTheSame(oldItem: CardBean, newItem: CardBean): Boolean { return oldItem == newItem } }) } override fun bindItem( item: CardBean, viewBinding: ModuleItemCardBinding, viewType: Int, position: Int ) { viewBinding.bindCard(item.card) // viewBinding.name.text = item.card.name // viewBinding.cost.text = item.card.cost.toString() viewBinding.count.text = item.count.toString() // item.card.rarity?.apply { // viewBinding.name.setTextColor( // ResourcesCompat.getColor( // viewBinding.root.context.resources, // colorRes, // null // ) // ) // // } // // Glide.with(viewBinding.imageTile) // .load("https://art.hearthstonejson.com/v1/tiles/${item.card.id}.png") // .into(viewBinding.imageTile) } override fun createViewBinding( inflater: LayoutInflater, parent: ViewGroup, viewType: Int ): ModuleItemCardBinding { return ModuleItemCardBinding.inflate(inflater, parent, false) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/common/LoadingFragment.kt ================================================ package com.ke.hs_tracker.module.ui.common import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.databinding.ModuleFragmentLoadingBinding class LoadingFragment : Fragment() { private val binding:ModuleFragmentLoadingBinding by viewbind() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deck/DeckCodeParserActivity.kt ================================================ package com.ke.hs_tracker.module.ui.deck import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.DividerItemDecoration import com.ke.hs_tracker.module.databinding.ModuleActivityDeckCodeParserBinding import com.ke.hs_tracker.module.ui.common.CardAdapter import com.ke.mvvm.base.data.ViewStatus import com.ke.mvvm.base.ui.collectLoadingDialog import com.ke.mvvm.base.ui.collectSnackbarFlow import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class DeckCodeParserActivity : AppCompatActivity() { private val viewModel: DeckCodeParserViewModel by viewModels() private lateinit var binding: ModuleActivityDeckCodeParserBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityDeckCodeParserBinding.inflate(layoutInflater) setContentView(binding.root) collectLoadingDialog(viewModel) collectSnackbarFlow(viewModel) binding.recyclerView.addItemDecoration( DividerItemDecoration( this, DividerItemDecoration.VERTICAL ) ) val adapter = CardAdapter() binding.recyclerView.adapter = adapter binding.start.setOnClickListener { viewModel.start( binding.code.text?.toString() ?: "" ) } binding.toolbar.setNavigationOnClickListener { onBackPressed() } launchAndRepeatWithViewLifecycle { viewModel.viewStatus.collect { when (it) { is ViewStatus.Loading -> { } is ViewStatus.Content -> { adapter.setList(it.data.apply { }) } is ViewStatus.Error -> { } } } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deck/DeckCodeParserViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.deck import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase import com.ke.hs_tracker.module.entity.CardBean import com.ke.mvvm.base.data.successOr import com.ke.mvvm.base.ui.BaseContentViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class DeckCodeParserViewModel @Inject constructor(private val parseDeckCodeUseCase: ParseDeckCodeUseCase) : BaseContentViewModel>() { fun start(code: String) { viewModelScope.launch { showLoadingDialog("加载中") val list = parseDeckCodeUseCase(code).successOr(emptyList()) dismissLoadingDialog() showContent(list) // when (result) { // is Result.Success -> { // showContent(result.data) // } // is Result.Error -> { // result.exception.printStackTrace() // } // } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/BattleRecordsFragment.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.DividerItemDecoration import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.ui.records.RecordAdapter import com.ke.mvvm.base.databinding.KeMvvmLayoutBaseRefreshListRetryBinding import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class BattleRecordsFragment : Fragment() { private val battleRecordsViewModel: BattleRecordsViewModel by activityViewModels() private val binding: KeMvvmLayoutBaseRefreshListRetryBinding by viewbind() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } private val adapter by lazy { RecordAdapter() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.swipeRefreshLayout.isEnabled = false binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration( DividerItemDecoration( requireContext(), DividerItemDecoration.VERTICAL ) ) launchAndRepeatWithViewLifecycle { battleRecordsViewModel.games.collect { adapter.setList(it) } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/BattleRecordsViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.db.Game import com.ke.mvvm.base.data.successOr import com.ke.mvvm.base.ui.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class BattleRecordsViewModel @Inject constructor( private val getGamesByDeckCodeAndNameUseCase: GetGamesByDeckCodeAndNameUseCase, savedStateHandle: SavedStateHandle ) : BaseViewModel() { private val _games = MutableStateFlow>(emptyList()) internal val games: StateFlow> get() = _games init { viewModelScope.launch { val code = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_CODE)!! val name = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_NAME)!! _games.value = getGamesByDeckCodeAndNameUseCase(code to name).successOr(emptyList()) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckBattleDetailActivity.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayoutMediator import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivityDeckBattleDetailBinding import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class DeckBattleDetailActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ModuleActivityDeckBattleDetailBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } val fragmentList = listOf( SummaryFragment(), BattleRecordsFragment(), DeckFragment(), DeckDetailFragment() ) val titles = listOf( getString(R.string.module_summary), getString(R.string.module_record), getString(R.string.module_deck), getString( R.string.module_deck_detail ) ) val adapter = object : FragmentStateAdapter(this) { override fun getItemCount(): Int { return fragmentList.size } override fun createFragment(position: Int): Fragment { return fragmentList[position] } } binding.viewPager.adapter = adapter TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, index -> tab.text = titles[index] }.attach() } companion object { fun createIntent(context: Context, deckCode: String, deckName: String): Intent { return Intent(context, DeckBattleDetailActivity::class.java).apply { putExtra(EXTRA_DECK_NAME, deckName) putExtra(EXTRA_DECK_CODE, deckCode) } } internal const val EXTRA_DECK_CODE = "EXTRA_DECK_CODE" internal const val EXTRA_DECK_NAME = "EXTRA_DECK_NAME" } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckDetailFragment.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarEntry import com.github.mikephil.charting.formatter.ValueFormatter import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.databinding.ModuleFragmentDeckBattleDetailDeckDetailBinding import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class DeckDetailFragment : Fragment() { private val binding: ModuleFragmentDeckBattleDetailDeckDetailBinding by viewbind() private val deckDetailViewModel: DeckDetailViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) launchAndRepeatWithViewLifecycle { deckDetailViewModel.costList.collect { fillChart( binding.costChart, it ) } } launchAndRepeatWithViewLifecycle { deckDetailViewModel.attackList.collect { fillChart( binding.attackChart, it ) } } launchAndRepeatWithViewLifecycle { deckDetailViewModel.mechanics.collect { list -> fillChips( binding.mechanicsChips, list.map { it.first.name + it.second } ) } } launchAndRepeatWithViewLifecycle { deckDetailViewModel.raceList.collect { list -> fillChips(binding.raceChips, list.map { getString(it.first.titleRes!!) + it.second }) } } launchAndRepeatWithViewLifecycle { deckDetailViewModel.spellSchoolList.collect { list -> fillChips( binding.spellSchoolChips, list.map { getString(it.first.titleRes) + it.second } ) } } } private fun fillChips(chipGroup: ChipGroup, strings: List) { chipGroup.removeAllViews() strings.map { Chip(requireContext()).apply { text = it } }.forEach { chipGroup.addView(it) } } private fun fillChart(barChart: BarChart, list: List>) { barChart.description.isEnabled = false barChart.xAxis.apply { setDrawGridLines(false) position = XAxis.XAxisPosition.BOTTOM labelCount = list.size textColor = Color.WHITE } barChart.axisLeft.apply { setDrawAxisLine(false) } barChart.legend.isEnabled = false val entryList = list.map { BarEntry(it.first.toFloat(), it.second.toFloat()) } val barDataSet = BarDataSet(entryList, "") // barDataSet.setValueTextColors(listOf(Color.WHITE)) barDataSet.valueTextColor = Color.WHITE barDataSet.valueTextSize = 14f barDataSet.valueFormatter = object : ValueFormatter() { override fun getFormattedValue(value: Float): String { return value.toInt().toString() } } barChart.data = BarData(barDataSet) barChart.setFitBars(true) barChart.invalidate() } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckDetailViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase import com.ke.hs_tracker.module.entity.CardType import com.ke.hs_tracker.module.entity.Mechanics import com.ke.hs_tracker.module.entity.Race import com.ke.hs_tracker.module.entity.SpellSchool import com.ke.mvvm.base.data.successOr import com.ke.mvvm.base.ui.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class DeckDetailViewModel @Inject constructor( private val parseDeckCodeUseCase: ParseDeckCodeUseCase, savedStateHandle: SavedStateHandle ) : BaseViewModel() { private val _costList = MutableStateFlow>>(emptyList()) /** * 法力曲线 */ internal val costList: StateFlow>> get() = _costList private val _attackList = MutableStateFlow>>(emptyList()) /** * 攻击力曲线 */ internal val attackList: StateFlow>> get() = _attackList private val _mechanics = MutableStateFlow>>(emptyList()) /** * 卡牌效果 */ internal val mechanics: StateFlow>> get() = _mechanics private val _raceList = MutableStateFlow>>(emptyList()) //随从种族 internal val raceList: StateFlow>> get() = _raceList private val _spellSchoolList = MutableStateFlow>>(emptyList()) internal val spellSchoolList: StateFlow>> get() = _spellSchoolList init { viewModelScope.launch { val code = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_CODE)!! val cardList = parseDeckCodeUseCase(code).successOr(emptyList()) _costList.value = cardList .flatMap { it.toCardList() } .groupBy { val cost = it.cost if (cost > MAX_VALUE) MAX_VALUE else cost } .map { it.key to it.value.size } _attackList.value = cardList .flatMap { it.toCardList() } .filter { //必须是随从牌或武器牌 it.type == CardType.Minion || it.type == CardType.Weapon }.groupBy { if ( it.attack >= MAX_VALUE ) MAX_VALUE else it.attack }.map { it.key to it.value.size } _mechanics.value = cardList .flatMap { it.toCardList() } .flatMap { it.mechanics }.groupBy { it }.map { it.key to it.value.size } val raceMap = mutableMapOf() var allRace = 0 cardList.flatMap { it.toCardList() }.mapNotNull { it.race }.forEach { if (it.tradition) { if (it != Race.All) { var count = raceMap[it] ?: 0 count += 1 raceMap[it] = count } else if (it == Race.All) { allRace += 1 } } } _raceList.value = raceMap.map { it.key to it.value + allRace } _spellSchoolList.value = cardList.flatMap { it.toCardList() }.mapNotNull { it.spellSchool }.groupBy { it }.map { it.key to it.value.size } } } } private const val MAX_VALUE = 7 ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckFragment.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import androidx.fragment.app.activityViewModels import com.ke.hs_tracker.module.entity.CardBean import com.ke.hs_tracker.module.ui.main.CardListFragment import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.StateFlow @AndroidEntryPoint class DeckFragment : CardListFragment() { private val deckViewModel: DeckViewModel by activityViewModels() override val cardList: StateFlow> get() = deckViewModel.cardList } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase import com.ke.hs_tracker.module.entity.CardBean import com.ke.mvvm.base.data.successOr import com.ke.mvvm.base.ui.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class DeckViewModel @Inject constructor( private val parseDeckCodeUseCase: ParseDeckCodeUseCase, savedStateHandle: SavedStateHandle ) : BaseViewModel() { private val _cardList = MutableStateFlow(emptyList()) internal val cardList: StateFlow> get() = _cardList init { viewModelScope.launch { val code = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_CODE)!! _cardList.value = parseDeckCodeUseCase(code).successOr(emptyList()) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/GetGamesByDeckCodeAndNameUseCase.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import com.ke.hs_tracker.module.db.Game import com.ke.hs_tracker.module.db.GameDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class GetGamesByDeckCodeAndNameUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, private val gameDao: GameDao ) : UseCase, List>(dispatcher) { override suspend fun execute(parameters: Pair): List { return gameDao.getByDeckCodeAndName( parameters.first, parameters.second ) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/SummaryFragment.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.github.mikephil.charting.charts.PieChart import com.github.mikephil.charting.components.Legend import com.github.mikephil.charting.data.PieData import com.github.mikephil.charting.data.PieDataSet import com.github.mikephil.charting.data.PieEntry import com.github.mikephil.charting.formatter.PercentFormatter import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleFragmentDeckBattleDetailSummaryBinding import com.ke.hs_tracker.module.ui.chart.PieChartData import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect /** * 总览 */ @AndroidEntryPoint internal class SummaryFragment : Fragment() { private val binding: ModuleFragmentDeckBattleDetailSummaryBinding by viewbind() private val summaryViewModel: SummaryViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) launchAndRepeatWithViewLifecycle { summaryViewModel.heroBattleItems.collect { list -> setupPieChart( list.map { PieChartData( getString(it.first.titleRes) + it.second, it.second, it.first.color ) }, binding.chart ) } } } private fun setupPieChart(list: List, pieChart: PieChart) { pieChart.apply { //设置成实心 holeRadius = 0f transparentCircleRadius = 0f //禁用描述 description.isEnabled = false legend.isEnabled = true legend.textColor = resources.getColor(R.color.module_grey500, null) legend.orientation = Legend.LegendOrientation.VERTICAL legend.horizontalAlignment = Legend.LegendHorizontalAlignment.LEFT legend.verticalAlignment = Legend.LegendVerticalAlignment.TOP setEntryLabelColor(resources.getColor(R.color.module_grey500, null)) setUsePercentValues(true) val pieDataSet = PieDataSet(list.map { return@map PieEntry( it.value.toFloat(), it.label ) }, "") pieDataSet.valueTextSize = 16f pieDataSet.valueTextColor = resources.getColor(R.color.module_grey500, null) pieDataSet.colors = list.map { it.color }.map { resources.getColor(it, null) } val pieData = PieData(pieDataSet) // pieData.setDrawValues(true) pieData.setValueFormatter(PercentFormatter(this)) // pieData.setValueFormatter(object : ValueFormatter() { // override fun getFormattedValue(value: Float): String { // return "$value %" // } // }) data = pieData invalidate() } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/SummaryViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.deckbattledetail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.entity.CardClass import com.ke.mvvm.base.data.successOr import com.ke.mvvm.base.ui.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SummaryViewModel @Inject constructor( private val getGamesByDeckCodeAndNameUseCase: GetGamesByDeckCodeAndNameUseCase, savedStateHandle: SavedStateHandle ) : BaseViewModel() { private val _heroBattleItems = MutableStateFlow>>(emptyList()) internal val heroBattleItems: StateFlow>> get() = _heroBattleItems init { viewModelScope.launch { val code = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_CODE)!! val name = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_NAME)!! val list = getGamesByDeckCodeAndNameUseCase(code to name).successOr(emptyList()) _heroBattleItems.value = list.groupBy { it.opponentHero!! }.map { it.key to it.value.count() } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/diagnose/DiagnoseActivity.kt ================================================ package com.ke.hs_tracker.module.ui.diagnose import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivityDiagnoseBinding import com.ke.hs_tracker.module.findHSDataFilesDir /** * 诊断 */ class DiagnoseActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setContentView(R.layout.module_activity_diagnose) val binding = ModuleActivityDiagnoseBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } binding.checkConfigFile.setOnClickListener { var message = "" val logsDir = findHSDataFilesDir("log.config") if (logsDir == null) { message = "无法找到log.config文件" } else { } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/filter/FilterActivity.kt ================================================ package com.ke.hs_tracker.module.ui.filter import android.content.Context import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivityFilterBinding import com.ke.hs_tracker.module.databinding.ModuleItemChipFilterBinding import com.ke.hs_tracker.module.entity.Card import com.ke.hs_tracker.module.entity.CardClass import com.ke.hs_tracker.module.entity.CardType class FilterActivity : AppCompatActivity() { private lateinit var binding: ModuleActivityFilterBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityFilterBinding.inflate(layoutInflater) setContentView(binding.root) val cardList = intent.getParcelableArrayListExtra(EXTRA_CARD_LIST)?.toList() ?: throw RuntimeException("cardList 不能为空") if (cardList.isEmpty()) { finish() } binding.radioGroup.setOnCheckedChangeListener { _, id -> binding.chipGroupClass.isVisible = id == R.id.rb_class binding.chipGroupRarity.isVisible = id == R.id.rb_rarity binding.chipGroupRace.isVisible = id == R.id.rb_minion binding.chipGroupSpell.isVisible = id == R.id.rb_spell binding.chipGroupWeapon.isVisible = id == R.id.rb_weapon binding.chipGroupHero.isVisible = id == R.id.rb_hero binding.chipGroupMechanics.isVisible = id == R.id.rb_mechanics } binding.toolbar.setNavigationOnClickListener { onBackPressed() } // val cardClasses: List> = cardList.map { if (it.classes.isEmpty() && it.cardClass != null) { listOf(it.cardClass) } else if (it.classes.isNotEmpty()) { it.classes } else { emptyList() } }.filter { it.isNotEmpty() }.groupBy { it }.map { it.key to it.value.size }.forEach { val b = ModuleItemChipFilterBinding.inflate(layoutInflater) b.root.setText( "${getName(it.first, applicationContext)} ${it.second}" ) b.root.tag = it.first binding.chipGroupClass .addView(b.root) } if (binding.chipGroupClass.childCount == 0) { binding.rbClass.isEnabled = false binding.rbClass.isChecked = false } else { binding.rbClass.isChecked = true } cardList.mapNotNull { it.rarity }.sortedBy { it.ordinal } .groupBy { it }.map { it.key to it.value.size }.forEach { val b = ModuleItemChipFilterBinding.inflate(layoutInflater) b.root.setText( "${getString(it.first.titleRes)} ${it.second}" ) binding.chipGroupRarity .addView(b.root) } cardList.mapNotNull { it.race }.groupBy { it }.map { it.key to it.value.size }.forEach { val b = ModuleItemChipFilterBinding.inflate(layoutInflater) b.root.setText( "${getString(it.first.titleRes!!)} ${it.second}" ) binding.chipGroupRace .addView(b.root) } if (binding.chipGroupRace.childCount == 0) { //没有随从 binding.rbMinion.isChecked = false binding.rbMinion.isEnabled = false } cardList.mapNotNull { it.spellSchool }.groupBy { it }.map { it.key to it.value.size }.forEach { val b = ModuleItemChipFilterBinding.inflate(layoutInflater) b.root.setText( "${getString(it.first.titleRes!!)} ${it.second}" ) binding.chipGroupSpell .addView(b.root) } if (binding.chipGroupSpell.childCount == 0) { binding.rbSpell.isChecked = false binding.rbSpell.isEnabled = false } val weaponCount = cardList.count { it.type == CardType.Weapon } if (weaponCount == 0) { binding.rbWeapon.isChecked = false binding.rbWeapon.isEnabled = false } else { val b = ModuleItemChipFilterBinding.inflate(layoutInflater) b.root.setText( "${getString(R.string.module_weapon)} ${weaponCount}" ) binding.chipGroupWeapon .addView(b.root) } val heroCount = cardList.count { it.type == CardType.Hero } if (heroCount == 0) { binding.rbHero.isChecked = false binding.rbHero.isEnabled = false } else { val b = ModuleItemChipFilterBinding.inflate(layoutInflater) b.root.setText( "${getString(R.string.module_hero)} ${heroCount}" ) binding.chipGroupHero .addView(b.root) } cardList.flatMap { it.mechanics }.groupBy { it } .map { it.key to it.value.size }.forEach { val b = ModuleItemChipFilterBinding.inflate(layoutInflater) b.root.setText( "${it.first.name} ${it.second}" ) binding.chipGroupMechanics .addView(b.root) } if (binding.chipGroupMechanics.childCount == 0) { binding.rbMechanics.isChecked = false binding.rbMechanics.isEnabled = false } // cardList.flatMap { // it.classes // }.filter { it.display } // .groupBy { // it // }.map { // it.key to it.value.size // } // cardList.groupBy { // it.cardClass!! // }.map { // it.key to it.value.count() // } // CardClass.values() // .filter { // it.display // } // .forEach { // val chip = // ModuleItemChipFilterBinding.inflate(layoutInflater).root // // val isBlackTextColor = it.blackText // // if (isBlackTextColor) { // chip.setTextColor(Color.BLACK) // chip.setCheckedIconResource(R.drawable.module_baseline_done_black_24dp) // } else { // chip.setTextColor(Color.WHITE) // chip.setCheckedIconResource(R.drawable.module_baseline_done_white_24dp) // } // chip.setChipBackgroundColorResource(it.color) // chip.setText(it.titleRes) // binding.chipGroupClass.addView(chip) // } // // Rarity.values().forEach { // val chip = // ModuleItemChipFilterBinding.inflate(layoutInflater).root // chip.setText(it.title) // binding.chipGroupRarity.addView(chip) // } } companion object { const val EXTRA_CARD_LIST = "EXTRA_CARD_LIST" private fun getName(list: List, context: Context): String { if (list.isEmpty()) { throw RuntimeException("list 不能为空") } if (list.size == 1) { return context.getString(list.first().titleRes) } val stringBuilder = StringBuilder() stringBuilder.append("(") list.map { context.getString(it.titleRes) }.forEachIndexed { index, s -> stringBuilder.append(s) if (index != list.size - 1) { stringBuilder.append(" ") } } stringBuilder.append(")") return stringBuilder.toString() } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/CardListFragment.kt ================================================ package com.ke.hs_tracker.module.ui.main import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DividerItemDecoration import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.databinding.ModuleFragmentCardListBinding import com.ke.hs_tracker.module.databinding.ModuleItemCardBinding import com.ke.hs_tracker.module.entity.CardBean import com.ke.hs_tracker.module.ui.common.CardAdapter import com.ke.mvvm.base.ui.BaseViewBindingAdapter import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect abstract class CardListFragment : Fragment() { private val adapter by lazy { CardAdapter() } private val binding: ModuleFragmentCardListBinding by viewbind() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration( DividerItemDecoration( requireContext(), DividerItemDecoration.VERTICAL ) ) launchAndRepeatWithViewLifecycle { cardList.collect { adapter.setList(it) } } } abstract val cardList: StateFlow> } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/DeckCardListFragment.kt ================================================ package com.ke.hs_tracker.module.ui.main import androidx.fragment.app.activityViewModels import com.ke.hs_tracker.module.entity.CardBean import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.StateFlow @AndroidEntryPoint class DeckCardListFragment : CardListFragment() { private val mainViewModel: MainViewModel by activityViewModels() override val cardList: StateFlow> get() = mainViewModel.deckLeftCardList } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/GraveyardFragment.kt ================================================ package com.ke.hs_tracker.module.ui.main import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.DiffUtil import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.bindCard import com.ke.hs_tracker.module.databinding.ModuleFragmentGraveyardBinding import com.ke.hs_tracker.module.databinding.ModuleItemCardBinding import com.ke.hs_tracker.module.entity.GraveyardCard import com.ke.hs_tracker.module.showCardImageDialog import com.ke.hs_tracker.module.ui.filter.FilterActivity import com.ke.mvvm.base.ui.BaseViewBindingAdapter import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class GraveyardFragment : Fragment() { private val binding: ModuleFragmentGraveyardBinding by viewbind() private val mainViewModel: MainViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.sorted.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, index: Int, p3: Long) { mainViewModel.setSort(SortBy.values()[index]) } override fun onNothingSelected(p0: AdapterView<*>?) { } } binding.filter.setOnClickListener { val cardList = mainViewModel.graveyardCardList.value.map { it.card } // listOf( // "SCH_270",//始生研习 // "SCH_235",//衰变飞弹 // "EX1_610",//爆炸陷阱 // "TU5_CS2_029",//火球术 // "CFM_852",//玉莲帮密探 // "GIL_598",//苔丝 // "ULD_156t3",//暴龙王 // "GVG_021",//玛尔加尼斯 // "FP1_022",//空灵召唤者 // "CS2_024",//寒冰箭 // "SCH_427",//雷霆绽放 // "Story_09_BlastcrystalPotion",//爆晶药水 // "CORE_CS2_106",//炽炎战斧 // ) // .map { // mainViewModel.allCard.find { card -> // card.id == it // }!! // } val intent = Intent(requireContext(), FilterActivity::class.java) intent.putParcelableArrayListExtra( FilterActivity.EXTRA_CARD_LIST, arrayListOf().apply { addAll(cardList) }) startActivity(intent) } binding.toggle.addOnButtonCheckedListener { _, checkedId, isChecked -> if (isChecked) { mainViewModel.toggleShowUserGraveyard(checkedId == R.id.self) } } binding.recyclerView.adapter = adapter adapter.setDiffCallback(object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: GraveyardCard, newItem: GraveyardCard): Boolean { return oldItem == newItem } override fun areContentsTheSame( oldItem: GraveyardCard, newItem: GraveyardCard ): Boolean { return oldItem == newItem } }) adapter.setOnItemClickListener { _, _, position -> showCardImageDialog(requireContext(), adapter.getItem(position).card.id) } // launchAndRepeatWithViewLifecycle { // mainViewModel.sortBy.collect { sortBy -> // sortList(sortBy) // } // } launchAndRepeatWithViewLifecycle { mainViewModel.graveyardCardList.collect { adapter.setList(it) binding.filter.isEnabled = it.isNotEmpty() //更新数据后重新排序下 // sortList(mainViewModel.sortBy.value) } } launchAndRepeatWithViewLifecycle { mainViewModel.showUserGraveyardCardList.collect { binding.toggle.check(if (it) R.id.self else R.id.opponent) } } } // private fun sortList(sortBy: SortBy) { // // // binding.sorted.setSelection(SortBy.values().indexOf(sortBy)) // val list = adapter.data // val result = when (sortBy) { // SortBy.Cost -> list.sortedBy { // it.card.cost // } // SortBy.CostReverse -> { // list.sortedByDescending { // it.card.cost // } // } // SortBy.Time -> { // list.sortedBy { // it.time // } // } // SortBy.TimeReverse -> { // list.sortedByDescending { it.time } // } // } // // adapter.setList(result) // } private val adapter = object : BaseViewBindingAdapter() { override fun bindItem( item: GraveyardCard, viewBinding: ModuleItemCardBinding, viewType: Int, position: Int ) { viewBinding.bindCard(item.card) } override fun createViewBinding( inflater: LayoutInflater, parent: ViewGroup, viewType: Int ): ModuleItemCardBinding { return ModuleItemCardBinding.inflate(inflater, parent, false) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/MainActivity.kt ================================================ package com.ke.hs_tracker.module.ui.main import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayoutMediator import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivityMainBinding import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class MainActivity : AppCompatActivity() { // private val adapter = // object : BaseViewBindingAdapter() { // override fun bindItem( // item: CardBean, // viewBinding: ModuleItemCardBinding, // viewType: Int, // position: Int // ) { // viewBinding.name.text = item.cardEntity.name + " " + item.count // viewBinding.cost.text = item.cardEntity.cost.toString() // } // // override fun createViewBinding( // inflater: LayoutInflater, // parent: ViewGroup, // viewType: Int // ): ModuleItemCardBinding { // return ModuleItemCardBinding.inflate(layoutInflater, parent, false) // } // // } private val mainViewModel: MainViewModel by viewModels() private lateinit var binding: ModuleActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } val titleList = listOf( R.string.module_deck, R.string.module_graveyard, R.string.module_opponent_hand_card // R.string.module_opponent_graveyard ) val fragmentList = listOf( DeckCardListFragment(), GraveyardFragment(), OpponentHandCardsFragment() // UserGraveyardFragment(), // OpponentGraveyardFragment() ) val adapter = object : FragmentStateAdapter(this) { override fun getItemCount(): Int { return fragmentList.size } override fun createFragment(position: Int): Fragment { return fragmentList[position] } } binding.viewPager.adapter = adapter binding.viewPager.offscreenPageLimit = fragmentList.size TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, index -> tab.setText(titleList[index]) }.attach() // val lines = assets // .open("Power.log").reader() // .readLines() // .toMutableList().apply { // removeFirstOrNull() // }.toList() // val mLines = mutableListOf() // mLines.addAll(lines) // // val logsDir = getExternalFilesDir("Logs")!! // if (logsDir.exists()) { // val powerFile = File(logsDir, "Power.log") // val writer = FileOutputStream(powerFile, true).writer() // // binding.next.setOnClickListener { // // repeat(100) { // val line = mLines.removeFirstOrNull() // if (line != null) { // writer.append(line) // writer.appendLine() // writer.flush() // } // } // // } // binding.clear.setOnClickListener { // mLines.clear() // mLines.addAll(lines) // } // } // binding.recyclerView.adapter = adapter // launchAndRepeatWithViewLifecycle { // mainViewModel.deckLeftCardList.collect { // adapter.setList(it) // } // } launchAndRepeatWithViewLifecycle { mainViewModel.title.collect { binding.toolbar.title = it } } } } internal enum class SortBy { Cost, CostReverse, Time, TimeReverse } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/MainViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.main import android.annotation.SuppressLint import android.content.Context import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.db.* import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.domain.GetAllCardUseCase import com.ke.hs_tracker.module.domain.GetRealLogDirUseCase import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase import com.ke.hs_tracker.module.domain.SaveLogFileUseCase import com.ke.hs_tracker.module.entity.* import com.ke.hs_tracker.module.log import com.ke.hs_tracker.module.parser.DeckFileObserver import com.ke.hs_tracker.module.parser.PowerFileObserver import com.ke.hs_tracker.module.parser.PowerParser import com.ke.mvvm.base.data.successOr import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import java.io.InputStream import java.util.* import javax.inject.Inject @SuppressLint("StaticFieldLeak") @HiltViewModel class MainViewModel @Inject constructor( @IoDispatcher private val dispatcher: CoroutineDispatcher, @ApplicationContext private val context: Context, private val parseDeckCodeUseCase: ParseDeckCodeUseCase, private val getAllCardUseCase: GetAllCardUseCase, private val getLogDirUseCase: GetRealLogDirUseCase, private val gameDao: GameDao, private val zonePositionChangedEventDao: ZonePositionChangedEventDao, private val saveLogFileUseCase: SaveLogFileUseCase, private val powerParser: PowerParser ) : ViewModel() { private var user: PowerTag.GameState.PlayerMapping? = null private var opponent: PowerTag.GameState.PlayerMapping? = null private var currentTurn = 0 // private var logsDir: DocumentFile? = null private suspend fun getLogsDir(): DocumentFile? { return getLogDirUseCase(Unit).successOr(null) } private val _title = MutableStateFlow("标题") val title: StateFlow get() = _title private val _deckLeftCardList = MutableStateFlow>(emptyList()) val deckLeftCardList: StateFlow> get() = _deckLeftCardList private val _graveyardCardList = MutableStateFlow>(emptyList()) val graveyardCardList: StateFlow> get() = _graveyardCardList /** * 对手手牌 */ private val currentOpponentHandCards = mutableListOf() private val _opponentHandCards = MutableStateFlow>>("" to emptyList()) /** * 对手手牌 */ val opponentHandCards: StateFlow>> get() = _opponentHandCards private val userGraveyardCardList = mutableListOf() private val opponentGraveyardCardList = mutableListOf() private val _showUserGraveyardCardList = MutableStateFlow(true) /** * 是否显示用户墓地 */ internal val showUserGraveyardCardList: StateFlow get() = _showUserGraveyardCardList internal fun toggleShowUserGraveyard(showUser: Boolean) { _showUserGraveyardCardList.value = showUser // _graveyardCardList.value = // if (showUser) userGraveyardCardList else opponentGraveyardCardList updateGraveyardCardList() } private val _sortBy = MutableStateFlow(SortBy.Cost) // internal val sortBy: StateFlow // get() = _sortBy /** * 设置排序方式 */ internal fun setSort(sortBy: SortBy) { _sortBy.value = sortBy updateGraveyardCardList() } private fun updateGraveyardCardList() { val source = if (_showUserGraveyardCardList.value) userGraveyardCardList else opponentGraveyardCardList val result = when (_sortBy.value) { SortBy.Cost -> source.sortedBy { it.card.cost } SortBy.CostReverse -> source.sortedByDescending { it.card.cost } SortBy.Time -> source.sortedBy { it.time } SortBy.TimeReverse -> source.sortedByDescending { it.time } } _graveyardCardList.value = result } private val powerFileObserver: PowerFileObserver by lazy { PowerFileObserver(1500) { getFileStream("Power.log") } } private val deckFileObserver: DeckFileObserver by lazy { DeckFileObserver { getFileStream("Decks.log") } } private var currentDeck: CurrentDeck? = null private var game: Game = Game() // private val powerParser = PowerParserImpl() private var allCard: List = emptyList() private var deckCardList: List = emptyList() private val zoneChangedEventList = mutableListOf() private val entityIdAndCardIdMap = mutableMapOf() init { viewModelScope.launch { clearPowerFile() allCard = getAllCardUseCase(Unit).successOr(emptyList()) } viewModelScope.launch { _deckLeftCardList.collect { } } // viewModelScope.launch { // powerParser.powerTagFlow.collect { // handlePowerTag(it) // } // } powerParser.powerTagListener = { handlePowerTag(it) } viewModelScope.launch { delay(1000) deckFileObserver .start() .flowOn(Dispatchers.IO) .map { it to parseDeckCodeUseCase(it.code).successOr(emptyList()) } .collect { _deckLeftCardList.value = it.second _title.value = it.first.name deckCardList = it.second currentDeck = it.first } } viewModelScope.launch { powerFileObserver.start() .flowOn(dispatcher) .collect { it.forEach { line -> powerParser.parse(line) } } } } private fun handlePowerTag(tag: PowerTag) { when (tag) { is PowerTag.GameState.BuildNumber -> { game = Game(buildNumber = tag.number) game.userDeckName = currentDeck?.name ?: "" game.userDeckCode = currentDeck?.code ?: "" } is PowerTag.GameState.FormatType -> { game.formatType = tag.type.toFormatType } is PowerTag.GameState.GameType -> { game.gameType = tag.type.toGameType } is PowerTag.GameState.PlayerMapping -> { if (tag.isUser) { game.userName = tag.name user = tag // game.isUserFirst = tag.first } else { game.opponentName = tag.name opponent = tag } } is PowerTag.GameState.ScenarioID -> { game.scenarioID = tag.id.toInt() } is PowerTag.PowerTaskList.Block -> { tag.list.forEach { handlePowerTag(it) } } is PowerTag.PowerTaskList.CreateGame -> { //初始化卡牌 onGameStarted() } is PowerTag.PowerTaskList.FullEntity -> { // handleFullEntity(tag) handleFullEntity(tag) //FULL_ENTITY - Updating [entityName=萨尔 id=74 zone=PLAY zonePos=0 cardId=HERO_02 player=2] CardID=HERO_02 // tag=CONTROLLER value=2 // tag=CARDTYPE value=HERO // tag=HEALTH value=30 // tag=ZONE value=PLAY // tag=ENTITY_ID value=74 // tag=FACTION value=NEUTRAL // tag=RARITY value=FREE // tag=HERO_POWER value=687 // tag=SPAWN_TIME_COUNT value=1 tag.cardId?.apply { entityIdAndCardIdMap[tag.entity.id] = this } } is PowerTag.PowerTaskList.ShowEntity -> { handleShowEntity(tag) entityIdAndCardIdMap[tag.entity.id] = tag.cardId } is PowerTag.PowerTaskList.TagChange -> { handleTagChange(tag) } } // tag.toString().log() (tag as? ZoneUpdatable)?.apply { isUpdateZone(user?.id)?.let { val cardId = it.cardId ?: entityIdAndCardIdMap[it.entityId] // val event = ZonePositionChangedEvent( // entityId = it.first.id, // cardId = it.first.cardId, // isUser = it.first.player == user?.id, // currentZone = it.first.zone, // newZone = it.second, // currentPosition = it.first.zonePosition, // newPosition = it.first.zonePosition // ) zoneChangedEventList.add(it) if (it.currentZone == Zone.Deck && it.newZone != Zone.Deck && it.isUser) { //从牌库中抽取一张卡 //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=48 zone=DECK zonePos=0 cardId= player=2] // tag=CONTROLLER value=2 // tag=CARDTYPE value=SPELL // tag=TAG_LAST_KNOWN_COST_IN_HAND value=1 // tag=COST value=1 // tag=PREMIUM value=1 // tag=ZONE value=HAND // tag=ENTITY_ID value=48 // tag=ELITE value=1 // tag=CLASS value=SHAMAN // tag=RARITY value=LEGENDARY // tag=478 value=2 // tag=QUEST_PROGRESS_TOTAL value=3 // tag=676 value=1 // tag=839 value=1 // tag=1043 value=1 // tag=1068 value=0 // tag=QUEST_REWARD_DATABASE_ID value=64323 // tag=SPAWN_TIME_COUNT value=1 //或者 探底 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=3 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=0 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1037 value=1 //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE value=HAND //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=6 //探底抽上来的卡是没有cardId的 removeCardFromDeck(cardId) } else if (it.newZone == Zone.Deck && it.currentZone != Zone.Deck && it.isUser) { //当心探底 //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] CardID=DED_002 // tag=CONTROLLER value=1 // tag=CARDTYPE value=SPELL // tag=TAG_LAST_KNOWN_COST_IN_HAND value=2 // tag=COST value=2 // tag=ZONE value=DECK // tag=ENTITY_ID value=28 // tag=RARITY value=RARE // tag=DISCOVER value=1 // tag=478 value=1 // tag=1043 value=1 // tag=1068 value=0 // tag=USE_DISCOVER_VISUALS value=1 // tag=SPAWN_TIME_COUNT value=1 // tag=SPELL_SCHOOL value=1 // tag=1711 value=1 // tag=MINI_SET value=1 //有牌插入到牌库 //TAG_CHANGE Entity=[entityName=冷风 id=70 zone=HAND zonePos=2 cardId=AV_266 player=2] tag=ZONE value=DECK //会出现id为空的情况 //FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=17 zone=DECK zonePos=0 cardId= player=1] CardID= // tag=ZONE value=DECK // tag=CONTROLLER value=1 // tag=ENTITY_ID value=17 insertCardToDeck(cardId) } else if (it.currentZone == Zone.Play && it.newZone == Zone.Graveyard) { onGraveyardCardsChanged(cardId, it.isUser) } else if (it.newZone == Zone.Hand || it.currentZone == Zone.Hand) { //手牌 if (!it.isUser) { handleOpponentHandChanged(it) } } } } } private var lastZonePositionChangedEvent: ZonePositionChangedEvent? = null /** * 对手手牌发生变化 */ private fun handleOpponentHandChanged(event: ZonePositionChangedEvent) { if (lastZonePositionChangedEvent?.entityId == event.entityId && event.currentZone == event.newZone) { return } if (event.newZone == Zone.Hand) { //有卡牌入手 可能从牌库抽到手里 也可能改变了位置 val target = currentOpponentHandCards.firstOrNull { it.entityId == event.entityId } if (target == null) { //插入一张卡牌到list中 currentOpponentHandCards.add( OpponentHandCard( currentTurn, event.entityId, event.newPosition ) ) } else { target.position = event.newPosition } } else if (event.currentZone == Zone.Hand) { //手牌出去了一张 val target = currentOpponentHandCards.firstOrNull { it.entityId == event.entityId } if (target == null) { // } else { // target.position = event.newPosition currentOpponentHandCards.remove(target) } } lastZonePositionChangedEvent = event _opponentHandCards.value = UUID.randomUUID().toString() to currentOpponentHandCards "对手手牌发生了变化 $event ,当前回合是 $currentTurn , 对手手牌 $currentOpponentHandCards".log() } private fun handleFullEntity(fullEntity: PowerTag.PowerTaskList.FullEntity) { fullEntity.isUpdateHero()?.apply { val cardClass = allCard.find { it.id == second }?.cardClass ?: return if (first == user?.id) { game.userHero = cardClass } else if (first == opponent?.id) { game.opponentHero = cardClass } } } private fun handleTagChange(tagChange: PowerTag.PowerTaskList.TagChange) { tagChange.isTurnChanged()?.apply { currentTurn = this } if (tagChange.entity.player == user?.id && tagChange.entity.zone == Zone.Hand && tagChange.tag == "ZONE" && tagChange.value == "DECK") { //TAG_CHANGE Entity=[entityName=冷风 id=15 zone=HAND zonePos=3 cardId=AV_266 player=1] tag=ZONE value=DECK // insertCardToDeck(tagChange.entity.cardId!!) } else if (tagChange.tag == "ZONE" && tagChange.value == "GRAVEYARD" && tagChange.entity.zone == Zone.Play) { //TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD //随从死亡后进入墓地 //TAG_CHANGE Entity=[entityName=始生研习 id=63 zone=PLAY zonePos=0 cardId=SCH_270 player=2] tag=ZONE value=GRAVEYARD //打出法术 // onGraveyardCardsChanged(tagChange.entity) } else if (tagChange.isGameComplete) { "游戏结束了".log() onGameOver() } val pair = tagChange.isPlayerWonOrLost if (pair != null) { "有玩家胜利或失败了 $pair $game".log() if (pair.first == game.userName) { game.isUserWin = pair.second } else { game.opponentName = pair.first } } if (tagChange.tag == "FIRST_PLAYER" && tagChange.value == "1") { game.isUserFirst = tagChange.entity.entityName == game.userName } } /** * 游戏开始 */ private fun onGameStarted() { "游戏开始了".log() _deckLeftCardList.value = deckCardList game.startTime = System.currentTimeMillis() currentTurn = 0 currentOpponentHandCards.clear() _opponentHandCards.value = UUID.randomUUID().toString() to currentOpponentHandCards } private fun onGameOver() { viewModelScope.launch { //保存游戏 game.endTime = System.currentTimeMillis() gameDao.insert(game) zonePositionChangedEventDao.insertAll(zoneChangedEventList.map { it.cardId = entityIdAndCardIdMap[it.entityId] it.gameId = game.id it.cardName = allCard.find { card: Card -> it.cardId == card.id }?.name it }) entityIdAndCardIdMap.clear() zoneChangedEventList.clear() getFileStream("Power.log")?.apply { saveLogFileUseCase(game.id to this) } delay(1000) clearPowerFile() } _deckLeftCardList.value = deckCardList // _userGraveyardCardList.value = emptyList() // _opponentGraveyardCardList.value = emptyList() userGraveyardCardList.clear() opponentGraveyardCardList.clear() _graveyardCardList.value = emptyList() currentTurn = 0 _opponentHandCards.value = UUID.randomUUID().toString() to emptyList() } private fun handleShowEntity(showEntity: PowerTag.PowerTaskList.ShowEntity) { if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads["ZONE"].equals( "hand", true ) ) { //起始手牌 //SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=15 zone=DECK zonePos=0 cardId= player=1] CardID=AV_266 // tag=CONTROLLER value=1 // tag=CARDTYPE value=SPELL // tag=TAG_LAST_KNOWN_COST_IN_HAND value=1 // tag=COST value=1 // tag=PREMIUM value=1 // tag=ZONE value=HAND // tag=ENTITY_ID value=15 // tag=RARITY value=COMMON // tag=478 value=1 // tag=1043 value=1 // tag=1068 value=0 // tag=SPAWN_TIME_COUNT value=1 // tag=SPELL_SCHOOL value=3 // removeCardFromDeck(showEntity.cardId) } else if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads["ZONE"].equals( "GRAVEYARD", true ) ) { //爆牌 // 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}) // removeCardFromDeck(showEntity.cardId) } } private fun removeCardFromDeck(cardId: String?) { onDeckCardsChanged(cardId, false) } private fun insertCardToDeck(cardId: String?) { onDeckCardsChanged(cardId, true) } private fun onGraveyardCardsChanged(cardId: String?, isUser: Boolean) { //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=106 zone=PLAY zonePos=0 cardId= player=1] tag=ZONE value=GRAVEYARD //TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=5 zone=SECRET zonePos=0 cardId= player=1] tag=COST value=2 //如果对面打出一张奥秘拍 会直接进入墓地 // ?: throw RuntimeException("没有id $entity") if (cardId == null) { return } val card = findCardById(cardId) if (card.type == CardType.Enchantment) { // "衍生牌 $card 不能放到墓地去".log() return } // "插入一张牌到墓地 $card $entity".log() //TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD if (isUser) { // viewModelScope.launch { // updateCardList( // cardEntity, _userGraveyardCardList, true // ) // } userGraveyardCardList.add(GraveyardCard(card)) // _graveyardCardList.value = userGraveyardCardList } else { // viewModelScope.launch { // updateCardList( // cardEntity, _opponentGraveyardCardList, true // ) // } opponentGraveyardCardList.add(GraveyardCard(card)) // _graveyardCardList.value = opponentGraveyardCardList } if (isUser && _showUserGraveyardCardList.value) { //是当前用户的卡插入到墓地 并且显示是当前用户的墓地 // _graveyardCardList.value = userGraveyardCardList updateGraveyardCardList() } else if (!isUser && _showUserGraveyardCardList.value) { // _graveyardCardList.value = opponentGraveyardCardList updateGraveyardCardList() } } private fun onDeckCardsChanged(cardId: String?, insert: Boolean) { if (cardId == null) { return } val card = findCardById(cardId) updateCardList(card, _deckLeftCardList, insert) } private fun findCardById(cardId: String) = allCard.find { it.id == cardId } ?: throw RuntimeException("id为 $cardId 没有这张牌") private suspend fun clearPowerFile() { withContext(dispatcher) { val documentFile = getLogsDir()?.findFile(powerFileName) documentFile?.apply { context.contentResolver.openOutputStream(uri, "wt")?.use { it.write("".encodeToByteArray()) it.flush() it.close() } } powerFileObserver.reset() deckFileObserver.reset() } } private suspend fun getFileStream(fileName: String): InputStream? = withContext(dispatcher) { val documentFile = getLogsDir()?.findFile(fileName) if (documentFile == null) { "无法访问 $fileName 文件".log() return@withContext null } context.contentResolver.openInputStream(documentFile.uri) } companion object { fun updateCardList( card: Card, mutableStateFlow: MutableStateFlow>, insert: Boolean, ) { if (card.type == CardType.Enchantment) { return } val list = mutableStateFlow.value.toMutableList() val bean = list.find { it.card.id == card.id } if (bean == null) { list.add(CardBean(card, 1)) } else { val newCount = if (!insert) bean.count - 1 else bean.count + 1 list[list.indexOf(bean)] = bean.updateCount(newCount) } if (bean?.count == 3) { "插入了3张进去? ".log() } mutableStateFlow.value = list.sortedBy { it.card.cost }.filter { it.count != 0 } } } } const val powerFileName = "Power.log" data class OpponentHandCard( /** * 回合数 */ val turn: Int, /** * 实体id */ val entityId: Int, /** * 位置 */ var position: Int, /** * 时间 */ val time: Long = System.currentTimeMillis() ) ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/OpponentGraveyardFragment.kt ================================================ package com.ke.hs_tracker.module.ui.main import androidx.fragment.app.activityViewModels import com.ke.hs_tracker.module.entity.CardBean import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.StateFlow //@AndroidEntryPoint //class OpponentGraveyardFragment : CardListFragment() { // // private val mainViewModel: MainViewModel by activityViewModels() // // override val cardList: StateFlow> // get() = mainViewModel.opponentGraveyardCardList //} ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/OpponentHandCardsFragment.kt ================================================ package com.ke.hs_tracker.module.ui.main import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.databinding.ModuleFragmentOpponentHandCardsBinding import com.ke.hs_tracker.module.databinding.ModuleItemOpponentHandCardBinding import com.ke.mvvm.base.ui.BaseViewBindingAdapter import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class OpponentHandCardsFragment : Fragment() { private val mainViewModel: MainViewModel by activityViewModels() private val binding: ModuleFragmentOpponentHandCardsBinding by viewbind() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ) = binding.root private val adapter = object : BaseViewBindingAdapter() { override fun bindItem( item: OpponentHandCard, viewBinding: ModuleItemOpponentHandCardBinding, viewType: Int, position: Int ) { viewBinding.text.text = (item.turn).toString() } override fun createViewBinding( inflater: LayoutInflater, parent: ViewGroup, viewType: Int ): ModuleItemOpponentHandCardBinding { return ModuleItemOpponentHandCardBinding.inflate(inflater, parent, false) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.recyclerView.adapter = adapter launchAndRepeatWithViewLifecycle { mainViewModel.opponentHandCards.collect { adapter.setList( it.second .sortedBy { card -> card.position } .sortedBy { card -> card.time } ) } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/UserGraveyardFragment.kt ================================================ package com.ke.hs_tracker.module.ui.main import androidx.fragment.app.activityViewModels import com.ke.hs_tracker.module.entity.CardBean import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.StateFlow //@AndroidEntryPoint //class UserGraveyardFragment : CardListFragment() { // // private val mainViewModel: MainViewModel by activityViewModels() // // override val cardList: StateFlow> // get() = mainViewModel.userGraveyardCardList //} ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/migrate/MigrateDataConvert.kt ================================================ package com.ke.hs_tracker.module.ui.migrate import androidx.annotation.WorkerThread import com.ke.hs_tracker.module.db.* import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import javax.inject.Inject internal class MigrateDataConvert @Inject constructor( private val gameDao: GameDao, private val zonePositionChangedEventDao: ZonePositionChangedEventDao, private val moshi: Moshi ) { private val adapter = moshi.adapter(MigrateData::class.java) @WorkerThread suspend fun getJsonString(): String { val migrateData = MigrateData(DATABASE_VERSION, gameDao.getAll(), zonePositionChangedEventDao.getAll()) return adapter.toJson(migrateData) } suspend fun save(jsonString: String): Boolean { val data = adapter.fromJson(jsonString) ?: return false saveData(data) return true } @WorkerThread private suspend fun saveData(migrateData: MigrateData) { gameDao.insert(migrateData.games) zonePositionChangedEventDao.insertAll(migrateData.zonePositionChangedEvents) } } @JsonClass(generateAdapter = true) internal data class MigrateData( val version: Int, val games: List, val zonePositionChangedEvents: List ) ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/migrate/MigrateMainActivity.kt ================================================ package com.ke.hs_tracker.module.ui.migrate import android.content.Context import android.content.Intent import android.net.wifi.WifiManager import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.ke.hs_tracker.module.databinding.ModuleActivityMigrateMainBinding class MigrateMainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ModuleActivityMigrateMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } binding.toThisPhone.setOnClickListener { startActivity(Intent(this, SocketServerActivity::class.java)) finish() } binding.toAnotherPhone.setOnClickListener { startActivity(Intent(this, SocketClientActivity::class.java)) finish() } } } internal fun getLocalIPAddress(context: Context): String { val manager = context.applicationContext.getSystemService(AppCompatActivity.WIFI_SERVICE) as WifiManager return int2ip(manager.connectionInfo.ipAddress) } internal fun int2ip(ipInt: Int): String { val sb = StringBuilder() sb.append(ipInt and 0xFF).append(".") sb.append(ipInt shr 8 and 0xFF).append(".") sb.append(ipInt shr 16 and 0xFF).append(".") sb.append(ipInt shr 24 and 0xFF) return sb.toString() } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/migrate/SocketClientActivity.kt ================================================ package com.ke.hs_tracker.module.ui.migrate import android.app.ProgressDialog import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.ke.hs_tracker.module.databinding.ModuleActivitySocketClientBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.net.Socket import javax.inject.Inject @AndroidEntryPoint class SocketClientActivity : AppCompatActivity() { @Inject internal lateinit var migrateDataConvert: MigrateDataConvert override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setContentView(R.layout.module_activity_socket_client) val binding: ModuleActivitySocketClientBinding = ModuleActivitySocketClientBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } // binding.ipAddress.setText(getLocalIPAddress(applicationContext)) binding.start.setOnClickListener { val progressDialog = ProgressDialog(this) progressDialog.show() lifecycleScope.launch(Dispatchers.IO) { val host = binding.ipAddress.text?.toString() ?: return@launch val port = binding.ipPort.text?.toString()?.toInt() ?: return@launch try { val socket = Socket(host, port) val text = migrateDataConvert.getJsonString() socket.getOutputStream().apply { write( text.toByteArray() ) flush() close() } runOnUiThread { progressDialog.dismiss() finish() } } catch (e: Exception) { runOnUiThread { progressDialog.dismiss() } e.printStackTrace() } } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/migrate/SocketServerActivity.kt ================================================ package com.ke.hs_tracker.module.ui.migrate import android.app.ProgressDialog import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.ke.hs_tracker.module.databinding.ModuleActivitySocketServerBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.net.ServerSocket import javax.inject.Inject @AndroidEntryPoint class SocketServerActivity : AppCompatActivity() { @Inject internal lateinit var migrateDataConvert: MigrateDataConvert override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setContentView(R.layout.module_activity_socket_server) val binding = ModuleActivitySocketServerBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } binding.ipAddress.setText(getLocalIPAddress(applicationContext)) binding.start.setOnClickListener { val port = binding.ipPort.text.toString().toIntOrNull() ?: return@setOnClickListener it.isEnabled = false binding.loadingProgress.isVisible = true start(port) } } private fun start(port: Int) { val progressDialog = ProgressDialog(this) progressDialog.show() lifecycleScope.launch { withContext(Dispatchers.IO) { val serverSocket = ServerSocket(port) val socket = serverSocket.accept() val text = socket.getInputStream().bufferedReader().readLine() ?: "" // Logger.d("收到了数据 $text") migrateDataConvert.save(text) runOnUiThread { finish() } } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/permissions/PermissionsActivity.kt ================================================ package com.ke.hs_tracker.module.ui.permissions import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.DocumentsContract import android.view.View import androidx.activity.result.contract.ActivityResultContract import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.documentfile.provider.DocumentFile import com.ke.hs_tracker.module.HS_DATA_FILE_DIR import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.canReadDataDir import com.ke.hs_tracker.module.databinding.ModuleActivityPermissionsBinding import com.ke.hs_tracker.module.hasAllPermissions import com.ke.hs_tracker.module.ui.writeconfig.WriteConfigActivity import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class PermissionsActivity : AppCompatActivity() { private lateinit var binding: ModuleActivityPermissionsBinding private val viewModel: PermissionsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityPermissionsBinding.inflate(layoutInflater) setContentView(binding.root) setTitle(R.string.module_set_permissions) initView() launchAndRepeatWithViewLifecycle { viewModel.navigationActions.collect { // val intent = when (it) { // PermissionsNavigationAction.NavigateToMain -> { // Intent(this@PermissionsActivity, MainActivity::class.java) // } // PermissionsNavigationAction.NavigateToSync -> { // Intent(this@PermissionsActivity, SyncCardDataActivity::class.java) // } // } startActivity(Intent(this@PermissionsActivity, WriteConfigActivity::class.java)) finish() } } } private fun initView() { // val requestPermissionLauncher = // registerForActivityResult(ActivityResultContracts.RequestPermission()) { // // } val requestAccessDataDirLauncher = registerForActivityResult(RequestAccessDataDir()) { if (it != null) { contentResolver.takePersistableUriPermission( it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION ) } } // binding.step3.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // val launcher = registerForActivityResult(RequestManageAllFilesAccessPermission()) { // // } // binding.step3.setOnClickListener { // launcher.launch(Unit) // } // } val onClickListener = View.OnClickListener { when (it) { // binding.step1 -> { // requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) // } binding.step2 -> { requestAccessDataDirLauncher.launch(Unit) } binding.next -> { viewModel.next() } } } // binding.step1.setOnClickListener(onClickListener) binding.step2.setOnClickListener(onClickListener) binding.next.setOnClickListener(onClickListener) } override fun onResume() { super.onResume() binding.next.isEnabled = hasAllPermissions // val hasPermission = // checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED // if (hasPermission) { // binding.step1.isEnabled = false // binding.step1.setCompoundDrawablesRelativeWithIntrinsicBounds( // 0, // 0, // R.drawable.module_baseline_done_green_500_24dp, // 0 // ) // } else { // binding.step1.isEnabled = true // binding.step1.setCompoundDrawablesRelativeWithIntrinsicBounds( // 0, // 0, // R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp, // 0 // ) // } if (canReadDataDir) { binding.step2.isEnabled = false binding.step2.setCompoundDrawablesRelativeWithIntrinsicBounds( 0, 0, R.drawable.module_baseline_done_green_500_24dp, 0 ) } else { binding.step2.isEnabled = true binding.step2.setCompoundDrawablesRelativeWithIntrinsicBounds( 0, 0, R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp, 0 ) } // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // if (isExternalStorageManager()) { // binding.step3.isEnabled = false // binding.step3.setCompoundDrawablesRelativeWithIntrinsicBounds( // 0, // 0, // R.drawable.module_baseline_done_green_500_24dp, // 0 // ) // } else { // binding.step3.isEnabled = true // binding.step3.setCompoundDrawablesRelativeWithIntrinsicBounds( // 0, // 0, // R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp, // 0 // ) // } // } } } class RequestAccessDataDir : ActivityResultContract() { override fun createIntent(context: Context, input: Unit): Intent { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) intent.flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) val documentFile = DocumentFile.fromTreeUri(context.applicationContext, HS_DATA_FILE_DIR)!! intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.uri) return intent } override fun parseResult(resultCode: Int, intent: Intent?): Uri? { return intent?.data } } //@RequiresApi(Build.VERSION_CODES.R) //class RequestManageAllFilesAccessPermission : ActivityResultContract() { // override fun createIntent(context: Context, input: Unit): Intent { // val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) // intent.data = Uri.parse("package:${context.packageName}") // return intent // } // // override fun parseResult(resultCode: Int, intent: Intent?): Boolean { // return Environment.isExternalStorageManager() // } // //} ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/permissions/PermissionsViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.permissions import android.content.Context import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.domain.GetDatabaseCardCountUseCase import com.ke.hs_tracker.module.domain.WriteLogConfigFileUseCase import com.ke.mvvm.base.data.successOr import com.ke.mvvm.base.ui.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class PermissionsViewModel @Inject constructor( private val getDatabaseCardCountUseCase: GetDatabaseCardCountUseCase, private val writeLogConfigFileUseCase: WriteLogConfigFileUseCase ) : BaseViewModel() { private val _navigationActions = Channel(capacity = Channel.CONFLATED) val navigationActions: Flow get() = _navigationActions.receiveAsFlow() internal fun next() { viewModelScope.launch { _navigationActions.send(PermissionsNavigationAction.NavigateToWriteConfig) // writeLogConfigFileUseCase(true) // if (getDatabaseCardCountUseCase(Unit).successOr(0) == 0) { // _navigationActions.send(PermissionsNavigationAction.NavigateToSync) // } else { // _navigationActions.send(PermissionsNavigationAction.NavigateToMain) // } } } } sealed interface PermissionsNavigationAction { // data class ShowErrorDialog( // val message: String // ) : PermissionsNavigationAction // object NavigateToMain : PermissionsNavigationAction object NavigateToWriteConfig : PermissionsNavigationAction } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordAdapter.kt ================================================ package com.ke.hs_tracker.module.ui.records import android.view.LayoutInflater import android.view.ViewGroup import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleItemRecordBinding import com.ke.hs_tracker.module.db.Game import com.ke.mvvm.base.ui.BaseViewBindingAdapter import java.text.SimpleDateFormat import java.util.* class RecordAdapter : BaseViewBindingAdapter() { override fun bindItem( item: Game, viewBinding: ModuleItemRecordBinding, viewType: Int, position: Int ) { viewBinding.apply { userHero.setImageResource(item.userHero!!.roundIcon!!) opponentHero.setImageResource(item.opponentHero!!.roundIcon!!) date.text = simpleDateFormat.format(Date(item.startTime)) state.isEnabled = item.isUserWin ?: true val type = root.context.getString(item.formatType.title) + root.context.getString(item.gameType.title) gameType.text = type state.setText(if (item.isUserWin == true) R.string.module_win else R.string.module_loss) } } override fun createViewBinding( inflater: LayoutInflater, parent: ViewGroup, viewType: Int ): ModuleItemRecordBinding { return ModuleItemRecordBinding.inflate(inflater, parent, false) } } private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordsActivity.kt ================================================ package com.ke.hs_tracker.module.ui.records import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.DividerItemDecoration import com.ke.hs_tracker.module.databinding.ModuleActivityRecordsBinding import com.ke.hs_tracker.module.ui.zoneevents.ZoneEventsActivity import com.ke.mvvm.base.data.ViewStatus import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class RecordsActivity : AppCompatActivity() { private val adapter by lazy { RecordAdapter().apply { setOnItemClickListener { _, _, position -> startActivity(Intent(this@RecordsActivity, ZoneEventsActivity::class.java).apply { putExtra(ZoneEventsActivity.EXTRA_KEY_ID, getItem(position).id) }) } } } private val recordsViewModel: RecordsViewModel by viewModels() private lateinit var binding: ModuleActivityRecordsBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityRecordsBinding.inflate(layoutInflater) setContentView(binding.root) binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration( DividerItemDecoration( this, DividerItemDecoration.VERTICAL ) ) binding.toolbar.setNavigationOnClickListener { onBackPressed() } // binding.toolbar.menu.apply { // clear() // add( // 0, // 0, // 0, // R.string.module_settings // ).setIcon(R.drawable.module_baseline_settings_white_24dp) // .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) // // // } // binding.toolbar.setOnMenuItemClickListener { // startActivity(Intent(this, SettingsActivity::class.java)) // true // } binding.swipeRefreshLayout.setOnRefreshListener { recordsViewModel.loadData() } launchAndRepeatWithViewLifecycle { recordsViewModel.viewStatus.collect { binding.swipeRefreshLayout.isRefreshing = it is ViewStatus.Loading when (it) { is ViewStatus.Loading -> { } is ViewStatus.Content -> { adapter.setList(it.data) } is ViewStatus.Error -> { } } } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordsViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.records import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.db.Game import com.ke.hs_tracker.module.db.GameDao import com.ke.mvvm.base.ui.BaseContentViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class RecordsViewModel @Inject constructor(private val gameDao: GameDao) : BaseContentViewModel>() { init { loadData() } internal fun loadData() { viewModelScope.launch { showLoading() showContent(gameDao.getAll().filter { //掉线问题 it.opponentHero != null && it.isUserFirst != null }.sortedByDescending { it.endTime }) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/settings/SettingsActivity.kt ================================================ package com.ke.hs_tracker.module.ui.settings import android.content.Intent import android.net.Uri import android.os.Bundle import android.widget.CompoundButton import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.data.PreferenceStorage import com.ke.hs_tracker.module.databinding.ModuleActivitySettingsBinding import com.ke.hs_tracker.module.ui.deck.DeckCodeParserActivity import com.ke.hs_tracker.module.ui.migrate.MigrateMainActivity import com.ke.hs_tracker.module.ui.support.SupportActivity import com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity import com.ke.hs_tracker.module.ui.test.TestActivity import com.ke.hs_tracker.module.ui.theme.ThemeActivity import com.ke.hs_tracker.module.ui.writeconfig.WriteConfigActivity import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect import javax.inject.Inject @AndroidEntryPoint class SettingsActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener { private lateinit var binding: ModuleActivitySettingsBinding private val settingsViewModel: SettingsViewModel by viewModels() @Inject lateinit var preferenceStorage: PreferenceStorage override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivitySettingsBinding.inflate(layoutInflater) setContentView(binding.root) binding.floating.isChecked = preferenceStorage.floatingEnable binding.floating.setOnCheckedChangeListener { _, isChecked -> preferenceStorage.floatingEnable = isChecked } launchAndRepeatWithViewLifecycle { settingsViewModel.saveLogFileEnable.collect { binding.saveLogFile.setOnCheckedChangeListener(null) binding.saveLogFile.isChecked = it binding.saveLogFile.setOnCheckedChangeListener(this@SettingsActivity) } } binding.toolbar.apply { setNavigationOnClickListener { onBackPressed() } this.menu.add(0, 0, 0, "测试") this.setOnMenuItemClickListener { startActivity(Intent(this@SettingsActivity, TestActivity::class.java)) true } } binding.theme.setOnClickListener { startActivity(Intent(this, ThemeActivity::class.java)) } binding.codeParser.setOnClickListener { startActivity(Intent(this, DeckCodeParserActivity::class.java)) } binding.migrate.setOnClickListener { startActivity(Intent(this, MigrateMainActivity::class.java)) } binding.rewriteConfigFile.setOnClickListener { val intent = Intent(this, WriteConfigActivity::class.java) intent.putExtra(WriteConfigActivity.EXTRA_REWRITE, true) startActivity(intent) } binding.sync.setOnClickListener { val intent = Intent(this, SyncCardDataActivity::class.java) intent.putExtra(SyncCardDataActivity.EXTRA_SHOW_BACK_BUTTON, true) startActivity(intent) } //给作者发邮件 binding.llContactAuthor.setOnClickListener { val email = getString(R.string.module_author_email) val intent = Intent(Intent.ACTION_SEND) intent.type = "text/plain" intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) startActivity(Intent.createChooser(intent, "Send To")) } binding.llSource.setOnClickListener { val intent = Intent(Intent.ACTION_VIEW) intent.data = Uri.parse(getString(R.string.module_github_address)) startActivity(intent) } binding.support.setOnClickListener { startActivity(Intent(this, SupportActivity::class.java)) } } override fun onCheckedChanged(p0: CompoundButton, checked: Boolean) { settingsViewModel.setSaveLogFileEnable(checked) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/settings/SettingsViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.domain.GetSaveLogFileEnableUseCase import com.ke.hs_tracker.module.domain.SetSaveLogFileEnableUseCase import com.ke.mvvm.base.data.successOr import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( private val getSaveLogFileEnableUseCase: GetSaveLogFileEnableUseCase, private val setSaveLogFileEnableUseCase: SetSaveLogFileEnableUseCase ) : ViewModel() { private val _saveLogFileEnable = MutableStateFlow(false) internal val saveLogFileEnable: StateFlow get() = _saveLogFileEnable init { viewModelScope.launch { _saveLogFileEnable.value = getSaveLogFileEnableUseCase(Unit).successOr(false) } } internal fun setSaveLogFileEnable(enable: Boolean) { viewModelScope.launch { _saveLogFileEnable.value = setSaveLogFileEnableUseCase(enable).successOr(enable) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/splash/SplashActivity.kt ================================================ package com.ke.hs_tracker.module.ui.splash import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.ke.hs_tracker.module.ui.permissions.PermissionsActivity import com.ke.hs_tracker.module.ui.summary.SummaryActivity import com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint @SuppressLint("CustomSplashScreen") class SplashActivity : AppCompatActivity() { private val splashViewModel: SplashViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // val intent = if (hasAllPermissions) { // Intent(this, MainActivity::class.java) // } else { // Intent(this, PermissionsActivity::class.java) // } // startActivity(intent) launchAndRepeatWithViewLifecycle { // "写入log文件结果 ${writeLogFile()}".log() // val result = writeLogFile() // // "写入文件结果 $result".log() splashViewModel.navigationActions.collect { val clazz = when (it) { SplashNavigationAction.NavigateToMain -> SummaryActivity::class.java SplashNavigationAction.NavigateToPermissions -> PermissionsActivity::class.java SplashNavigationAction.NavigateToSync -> SyncCardDataActivity::class.java } val intent = Intent(this@SplashActivity, clazz) startActivity(intent) } } } // private suspend fun writeLogFile(): Boolean { // return withContext(Dispatchers.IO) { // // // val documentFile = findHSDataFilesDir("Logs") ?: return@withContext false // // val fileName = "Power.log" // documentFile?.findFile(fileName)?.delete() // val configFile = documentFile.createFile("plain/text", fileName) // ?: return@withContext false // // contentResolver.openOutputStream(configFile.uri)?.use { // assets.open("Power.log") // .copyTo(it) // it.flush() // } // // return@withContext true // } // // // } override fun onStop() { super.onStop() finish() } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/splash/SplashViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.splash import android.content.Context import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.domain.GetDatabaseCardCountUseCase import com.ke.hs_tracker.module.hasAllPermissions import com.ke.mvvm.base.data.successOr import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( @ApplicationContext context: Context, private val getDatabaseCardCountUseCase: GetDatabaseCardCountUseCase ) : ViewModel() { private val _navigationActions = Channel(capacity = Channel.CONFLATED) val navigationActions: Flow get() = _navigationActions.receiveAsFlow() init { viewModelScope.launch { if (context.hasAllPermissions) { if (getDatabaseCardCountUseCase(Unit).successOr(0) == 0) { _navigationActions.send(SplashNavigationAction.NavigateToSync) } else { _navigationActions.send(SplashNavigationAction.NavigateToMain) } } else { _navigationActions.send(SplashNavigationAction.NavigateToPermissions) } } } } sealed interface SplashNavigationAction { object NavigateToPermissions : SplashNavigationAction object NavigateToMain : SplashNavigationAction object NavigateToSync : SplashNavigationAction } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateItem.kt ================================================ package com.ke.hs_tracker.module.ui.summary import androidx.annotation.IntRange import com.ke.hs_tracker.module.entity.CardClass /** * 对局胜率 */ sealed interface BattleRateItem { /** * 胜率局数 */ val winCount: Int /** * 失败局数 */ val lostCount: Int /** * 总局数 */ val allCount: Int /** * 先手局数 */ val firstHandCount: Int /** * 胜率 */ @get:IntRange(from = 0, to = 100) val rate: Int /** * 职业对战胜率 */ data class ClassBattleRate( override val winCount: Int, override val lostCount: Int, override val firstHandCount: Int, val cardClass: CardClass ) : BattleRateItem { override val allCount: Int get() = winCount + lostCount override val rate: Int get() = if (allCount == 0) 0 else winCount * 100 / allCount } /** * 卡组对战胜率 */ data class DeckBattleRate( override val winCount: Int, override val lostCount: Int, override val firstHandCount: Int, val deckName: String, val deckCode: String ) : BattleRateItem { override val allCount: Int get() = winCount + lostCount override val rate: Int get() = winCount * 100 / allCount } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateItemAdapter.kt ================================================ package com.ke.hs_tracker.module.ui.summary import android.view.LayoutInflater import android.view.ViewGroup import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleItemSummaryBattleBinding import com.ke.hs_tracker.module.ui.classbattledetail.ClassBattleDetailActivity import com.ke.hs_tracker.module.ui.deckbattledetail.DeckBattleDetailActivity import com.ke.mvvm.base.ui.BaseViewBindingAdapter internal class BattleRateItemAdapter : BaseViewBindingAdapter() { override fun bindItem( item: BattleRateItem, viewBinding: ModuleItemSummaryBattleBinding, viewType: Int, position: Int ) { viewBinding.apply { allCount.text = "总:" + (item.lostCount + item.winCount).toString() winCount.text = "胜:" + item.winCount.toString() lostCount.text = "负:" + item.lostCount.toString() winRate.text = item.rate.toString() + "%" when (item) { is BattleRateItem.ClassBattleRate -> { image.setImageResource(item.cardClass.roundIcon!!) name.setText(item.cardClass.titleRes) root.setOnClickListener { it.context.startActivity( ClassBattleDetailActivity.createIntent( it.context, item.cardClass ) ) } } is BattleRateItem.DeckBattleRate -> { image.setImageResource(R.drawable.module_image_round_demon_hunter) name.text = item.deckName root.setOnClickListener { it.context.startActivity( DeckBattleDetailActivity.createIntent( it.context, item.deckCode, item.deckName ) ) } } } } } override fun createViewBinding( inflater: LayoutInflater, parent: ViewGroup, viewType: Int ): ModuleItemSummaryBattleBinding { return ModuleItemSummaryBattleBinding.inflate(inflater, parent, false) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateListFragment.kt ================================================ package com.ke.hs_tracker.module.ui.summary import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DividerItemDecoration import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.R import com.ke.mvvm.base.data.ViewStatus import com.ke.mvvm.base.databinding.KeMvvmLayoutBaseRefreshListRetryBinding import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import kotlinx.coroutines.flow.collect abstract class BattleRateListFragment : Fragment() { internal val adapter = BattleRateItemAdapter() private val binding: KeMvvmLayoutBaseRefreshListRetryBinding by viewbind() protected abstract val viewModel: BattleRateListViewModel<*> final override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycle.addObserver(viewModel) binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration( DividerItemDecoration( requireContext(), DividerItemDecoration.VERTICAL ) ) adapter.addFooterView( layoutInflater.inflate(R.layout.module_item_footer_with_fab, null) ) binding.swipeRefreshLayout.setOnRefreshListener { viewModel.refresh() } launchAndRepeatWithViewLifecycle { viewModel.viewStatus.collect { when (it) { is ViewStatus.Loading -> { binding.swipeRefreshLayout.isRefreshing = true } is ViewStatus.Content -> { binding.swipeRefreshLayout.isRefreshing = false adapter.setList(it.data.sortedByDescending { item -> item.rate }) } is ViewStatus.Error -> throw IllegalArgumentException("不该出现错误的情况") } } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateListViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.summary import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.ke.mvvm.base.data.successOr import com.ke.mvvm.base.domian.UseCase import com.ke.mvvm.base.ui.BaseContentViewModel import kotlinx.coroutines.launch abstract class BattleRateListViewModel : BaseContentViewModel>(), DefaultLifecycleObserver { protected abstract val getBattleRateListUseCase: UseCase> override fun onResume(owner: LifecycleOwner) { super.onResume(owner) refresh() } internal fun refresh() { viewModelScope.launch { showLoading() showContent(getBattleRateListUseCase(Unit).successOr(emptyList())) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/DeckBattleRateListViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.summary import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class DeckBattleRateListViewModel @Inject constructor(override val getBattleRateListUseCase: GetDeckBattleRateListUseCase) : BattleRateListViewModel() ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/GetDeckBattleRateListUseCase.kt ================================================ package com.ke.hs_tracker.module.ui.summary import com.ke.hs_tracker.module.db.GameDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class GetDeckBattleRateListUseCase @Inject constructor( private val gameDao: GameDao, @IoDispatcher dispatcher: CoroutineDispatcher ) : UseCase>(dispatcher) { override suspend fun execute(parameters: Unit): List { val games = gameDao.getAll() return games .filter { it.userDeckName.isNotEmpty() && it.userDeckCode.isNotEmpty() } .groupBy { it.userDeckName to it.userDeckCode }.map { map -> BattleRateItem.DeckBattleRate( map.value.count { it.isUserWin == true }, map.value.count { it.isUserWin == false }, map.value.count { it.isUserFirst == false }, map.key.first, map.key.second ) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/GetHeroBattleRateListUseCase.kt ================================================ package com.ke.hs_tracker.module.ui.summary import com.ke.hs_tracker.module.db.GameDao import com.ke.hs_tracker.module.di.IoDispatcher import com.ke.hs_tracker.module.entity.CardClass import com.ke.mvvm.base.domian.UseCase import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Inject class GetHeroBattleRateListUseCase @Inject constructor( @IoDispatcher dispatcher: CoroutineDispatcher, private val gameDao: GameDao ) : UseCase>(dispatcher) { override suspend fun execute(parameters: Unit): List { val list = mutableListOf() CardClass.values() .filter { it.isHero } .map { it to gameDao.getByHero(it) }.forEach { pair -> val heroWinCount = pair.second.count { it.isUserWin == true } val heroLostCount = pair.second.count { it.isUserWin == false } val firstHandCount = pair.second.count { it.isUserFirst == true } val item = BattleRateItem.ClassBattleRate( heroWinCount, heroLostCount, firstHandCount, pair.first ) list.add(item) } return list } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/HeroBattleRateListViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.summary import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class HeroBattleRateListViewModel @Inject constructor(override val getBattleRateListUseCase: GetHeroBattleRateListUseCase) : BattleRateListViewModel() ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/RateByDeckFragment.kt ================================================ package com.ke.hs_tracker.module.ui.summary import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class RateByDeckFragment : BattleRateListFragment() { override val viewModel: DeckBattleRateListViewModel by viewModels() } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/RateByHeroFragment.kt ================================================ package com.ke.hs_tracker.module.ui.summary import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class RateByHeroFragment : BattleRateListFragment() { override val viewModel: HeroBattleRateListViewModel by viewModels() } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/SummaryActivity.kt ================================================ package com.ke.hs_tracker.module.ui.summary import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.Settings import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayoutMediator import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.data.PreferenceStorage import com.ke.hs_tracker.module.databinding.ModuleActivitySummaryBinding import com.ke.hs_tracker.module.service.WindowService import com.ke.hs_tracker.module.ui.chart.SummaryChartActivity import com.ke.hs_tracker.module.ui.main.MainActivity import com.ke.hs_tracker.module.ui.records.RecordsActivity import com.ke.hs_tracker.module.ui.settings.SettingsActivity import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class SummaryActivity : AppCompatActivity() { @Inject lateinit var preferenceStorage: PreferenceStorage private lateinit var binding: ModuleActivitySummaryBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivitySummaryBinding.inflate(layoutInflater) setContentView(binding.root) // request() binding.toolbar.apply { menu.clear() menu.add( 0, 0, 0, R.string.module_settings ).setIcon(R.drawable.module_baseline_settings_white_24dp) .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) menu.add( 0, 2, 0, R.string.module_pie_chart ).setIcon(R.drawable.module_baseline_pie_chart_white_24dp) .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) menu.add(0, 1, 0, R.string.module_view_all_games) setOnMenuItemClickListener { when (it.itemId) { 0 -> { startActivity(Intent(this@SummaryActivity, SettingsActivity::class.java)) } 1 -> { startActivity(Intent(this@SummaryActivity, RecordsActivity::class.java)) } 2 -> { startActivity( Intent( this@SummaryActivity, SummaryChartActivity::class.java ) ) } } true } } binding.start.setOnClickListener { // startActivity(Intent(this, MainActivity::class.java)) // startService(Intent(this, WindowService::class.java)) if (preferenceStorage.floatingEnable) { // if (Settings.canDrawOverlays(applicationContext)) { startService(Intent(this, WindowService::class.java)) } else { startActivityForResult( Intent( Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName") ), 101 ) } } else { startActivity(Intent(this, MainActivity::class.java)) } } val fragments = listOf(RateByHeroFragment(), RateByDeckFragment()) val titles = listOf(R.string.module_by_class, R.string.module_by_deck) val adapter = object : FragmentStateAdapter(this) { override fun getItemCount() = fragments.size override fun createFragment(position: Int) = fragments[position] } binding.viewPager.adapter = adapter TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> tab.setText(titles[position]) }.attach() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 101 && Settings.canDrawOverlays(applicationContext)) { startService(Intent(this, WindowService::class.java)) } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/support/SupportActivity.kt ================================================ package com.ke.hs_tracker.module.ui.support import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import com.ke.hs_tracker.module.R class SupportActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.module_activity_support) findViewById(R.id.toolbar).setNavigationOnClickListener { onBackPressed() } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/sync/SyncCardDataActivity.kt ================================================ package com.ke.hs_tracker.module.ui.sync import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivitySyncCardDataBinding import com.ke.hs_tracker.module.ui.summary.SummaryActivity import com.ke.mvvm.base.ui.collectLoadingDialog import com.ke.mvvm.base.ui.collectSnackbarFlow import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class SyncCardDataActivity : AppCompatActivity() { private lateinit var binding: ModuleActivitySyncCardDataBinding private val viewModel: SyncCardDataViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivitySyncCardDataBinding.inflate(layoutInflater) setContentView(binding.root) if (viewModel.showBackButton) { binding.toolbar.setNavigationIcon(R.drawable.module_baseline_arrow_back_white_24dp) binding.toolbar.setNavigationOnClickListener { onBackPressed() } } collectLoadingDialog(viewModel) collectSnackbarFlow(viewModel) binding.sync.setOnClickListener { viewModel.sync( binding.version.text?.toString() ?: "", if (binding.chinese.isChecked) "zhCN" else "enUS" ) } launchAndRepeatWithViewLifecycle { viewModel.navigationActions.collect { val action: () -> Unit = when (it) { SyncCardDataNavigationAction.NavigateToBack -> { { onBackPressed() } } SyncCardDataNavigationAction.NavigateToMain -> { { val intent = Intent(this@SyncCardDataActivity, SummaryActivity::class.java) startActivity(intent) finish() } } } AlertDialog.Builder(this@SyncCardDataActivity) .setTitle(R.string.module_hint) .setMessage(R.string.module_sync_success) .setOnDismissListener { action() }.setPositiveButton(R.string.module_done, null) .show() } } } companion object { const val EXTRA_SHOW_BACK_BUTTON = "EXTRA_SHOW_BACK_BUTTON" } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/sync/SyncCardDataViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.sync import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.domain.ClearCardTableUseCase import com.ke.hs_tracker.module.domain.GetCardListUseCase import com.ke.hs_tracker.module.domain.InsertCardListToDatabaseUseCase import com.ke.mvvm.base.data.Result import com.ke.mvvm.base.model.SnackbarAction import com.ke.mvvm.base.ui.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SyncCardDataViewModel @Inject constructor( private val getCardListUseCase: GetCardListUseCase, private val clearCardTableUseCase: ClearCardTableUseCase, private val insertCardListToDatabaseUseCase: InsertCardListToDatabaseUseCase, savedStateHandle: SavedStateHandle ) : BaseViewModel() { internal val showBackButton = savedStateHandle.get(SyncCardDataActivity.EXTRA_SHOW_BACK_BUTTON) ?: false private val _navigationActions = Channel(capacity = Channel.CONFLATED) val navigationActions: Flow get() = _navigationActions.receiveAsFlow() fun sync( versionCode: String, region: String ) { viewModelScope.launch { showLoadingDialog("同步中") val code = versionCode.ifEmpty { "latest" } when (val result = getCardListUseCase(code to region)) { is Result.Success -> { clearCardTableUseCase(Unit) insertCardListToDatabaseUseCase(result.data) dismissLoadingDialog() _navigationActions.send(if (showBackButton) SyncCardDataNavigationAction.NavigateToBack else SyncCardDataNavigationAction.NavigateToMain) } is Result.Error -> { result.exception.printStackTrace() dismissLoadingDialog() showSnackbar(SnackbarAction("从服务器获取数据失败")) // throw result.exception } } } } } sealed interface SyncCardDataNavigationAction { /** * 去首页 */ object NavigateToMain : SyncCardDataNavigationAction /** * 返回上一个页面 */ object NavigateToBack : SyncCardDataNavigationAction } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/test/CreateRecordActivity.kt ================================================ package com.ke.hs_tracker.module.ui.test import android.content.DialogInterface import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivityCreateRecordBinding import com.ke.hs_tracker.module.db.Game import com.ke.hs_tracker.module.db.GameDao import com.ke.hs_tracker.module.entity.CardClass import com.ke.hs_tracker.module.entity.FormatType import com.ke.hs_tracker.module.entity.GameType import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint class CreateRecordActivity : AppCompatActivity() { @Inject lateinit var gameDao: GameDao private val heroClasses = CardClass.values().filter { it.roundIcon != null } private var userClass = heroClasses[1] private var opponentClass = heroClasses[3] lateinit var binding: ModuleActivityCreateRecordBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityCreateRecordBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } refreshUserClass() refreshOpponentClass() binding.titleUserClass.setOnClickListener { showClassPickerDialog { dialog, index -> dialog.dismiss() userClass = heroClasses[index] refreshUserClass() } } binding.titleOpponentClass.setOnClickListener { showClassPickerDialog { dialog, index -> dialog.dismiss() opponentClass = heroClasses[index] refreshOpponentClass() } } binding.create.setOnClickListener { val game = Game( buildNumber = binding.buildNumber.text?.toString() ?: "", gameType = getGameType(), formatType = getFormatType(), scenarioID = 2, userName = binding.username.text?.toString() ?: "", opponentName = binding.opponentName.text?.toString() ?: "", isUserFirst = binding.isUserFirst.isChecked, isUserWin = binding.isUserWon.isChecked, userHero = userClass, opponentHero = opponentClass, userDeckCode = "AAECAbr5AwSJiwS4oASlrQSEsAQN5boD6LoD77oDm84D8NQDieADiuADpOED0eEDjOQDj+QDr4AEz6wEAA==", userDeckName = "天胡", startTime = System.currentTimeMillis() - 10000, endTime = System.currentTimeMillis() ) lifecycleScope.launch { gameDao.insert(game) AlertDialog.Builder(this@CreateRecordActivity) .setTitle("提示") .setMessage("写入成功") .setPositiveButton("确定", null) .show() } } } private fun showClassPickerDialog(onSelected: (DialogInterface, Int) -> Unit) { AlertDialog.Builder(this) .setTitle("选择职业") .setSingleChoiceItems(heroClasses.map { getString(it.titleRes) }.toTypedArray(), heroClasses.indexOf(userClass)) { dialog, index -> onSelected(dialog, index) }.show() } private fun getGameType(): GameType { return if (binding.rbGameType.checkedRadioButtonId == R.id.game_type_ranked) GameType.Ranked else GameType.Casual } private fun getFormatType(): FormatType { return when (binding.rbFormatType.checkedRadioButtonId) { R.id.format_type_standard -> FormatType.Standard R.id.format_type_classic -> FormatType.Classic R.id.format_type_wild -> FormatType.Wild else -> FormatType.Unknown } } private fun refreshUserClass() { binding.userClass.setText(userClass.titleRes) } private fun refreshOpponentClass() { binding.opponentClass.setText(opponentClass.titleRes) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/test/LocalFileParserActivity.kt ================================================ package com.ke.hs_tracker.module.ui.test import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import com.ke.hs_tracker.module.R class LocalFileParserActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.module_activity_local_file_parser) } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/test/TestActivity.kt ================================================ package com.ke.hs_tracker.module.ui.test import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.ke.hs_tracker.module.databinding.ModuleActivityTestBinding import com.ke.hs_tracker.module.findHSDataFilesDir import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class TestActivity : AppCompatActivity() { private lateinit var binding: ModuleActivityTestBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityTestBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } binding.createRecord.setOnClickListener { startActivity(Intent(this, CreateRecordActivity::class.java)) } binding.clearLog.setOnClickListener { lifecycleScope.launch { withContext(Dispatchers.IO) { val documentFile = findHSDataFilesDir("Logs")?.findFile("Power.log") // getLogsDir()?.findFile(powerFileName) documentFile?.apply { contentResolver.openOutputStream(uri, "wt")?.use { it.write("".encodeToByteArray()) it.flush() it.close() } } } AlertDialog.Builder(this@TestActivity) .setTitle("提示") .setMessage("清除成功") .setPositiveButton("确定", null) .show() } } // val powerParser: PowerParser = PowerParserImpl() // val lineList = mutableListOf() // // lineList.clear() // lineList.addAll( // assets.open("Power.log").reader().readLines().toMutableList() // ) // // val logDocument = findHSDataFilesDir("Logs") // // // var document = logDocument?.findFile("Power.log") // if (document == null) { // document = logDocument?.createFile("plain/text", "Power.log") // } // // var writer: OutputStreamWriter? = null // document?.apply { // // writer = contentResolver.openOutputStream(uri, "wa") // ?.writer() // } // binding.clear.setOnClickListener { // document?.apply { // contentResolver.openOutputStream(uri, "wt")?.writer()?.write("") // // } // // assets.open("Power.log").reader().apply { // lineList.clear() // // lineList.addAll(readLines().toMutableList()) // close() // } // // // } // powerParser.powerTagListener = { // // // } // var counter = 0 // binding.next.setOnClickListener { //// val target = mutableListOf() // repeat(1024) { // counter++ // val line = lineList.removeFirstOrNull() // if (line != null) { //// target.add(line) // writer?.appendLine(line) //// powerParser.parse(line) // } // } // writer?.flush() // // // } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/theme/ThemeActivity.kt ================================================ package com.ke.hs_tracker.module.ui.theme import android.os.Bundle import android.widget.CompoundButton import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import com.ke.hs_tracker.module.data.PreferenceStorage import com.ke.hs_tracker.module.databinding.ModuleActivityThemeBinding import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class ThemeActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener { @Inject lateinit var preferenceStorage: PreferenceStorage override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ModuleActivityThemeBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } binding.light.tag = AppCompatDelegate.MODE_NIGHT_NO binding.dark.tag = AppCompatDelegate.MODE_NIGHT_YES binding.system.tag = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM val buttonList = listOf( binding.light, binding.dark, binding.system ) buttonList.forEach { it.isChecked = it.tag == preferenceStorage.theme } buttonList.forEach { it.setOnCheckedChangeListener(this) } } override fun onCheckedChanged(button: CompoundButton, checked: Boolean) { if (checked) { val theme = button.tag as? Int ?: return preferenceStorage.theme = theme } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/writeconfig/WriteConfigActivity.kt ================================================ package com.ke.hs_tracker.module.ui.writeconfig import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivityWriteConfigBinding import com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity import com.ke.hs_tracker.module.writeLogConfigFile import kotlinx.coroutines.launch class WriteConfigActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setContentView(R.layout.module_activity_write_config) val binding = ModuleActivityWriteConfigBinding.inflate(layoutInflater) setContentView(binding.root) val rewrite = intent.getBooleanExtra(EXTRA_REWRITE, false) if (rewrite) { binding.forceWrite.isEnabled = false binding.forceWrite.isChecked = true binding.toolbar.apply { setNavigationIcon(R.drawable.module_baseline_arrow_back_white_24dp) setNavigationOnClickListener { onBackPressed() } } } binding.content.text = assets.open("log.config").reader().readText() binding.writeIn.setOnClickListener { // findHSDataFilesDir("log.config") lifecycleScope.launch { if (writeLogConfigFile( binding.forceWrite.isChecked ) ) { if (rewrite) { onBackPressed() } else { startActivity( Intent( this@WriteConfigActivity, SyncCardDataActivity::class.java ) ) finish() } } else { AlertDialog.Builder(this@WriteConfigActivity) .setTitle(R.string.module_hint) .setMessage(R.string.module_write_config_failed) .setPositiveButton(R.string.module_done, null) .show() } } } } companion object { const val EXTRA_REWRITE = "EXTRA_REWRITE" } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/zonecards/ZoneCardsActivity.kt ================================================ package com.ke.hs_tracker.module.ui.zonecards import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.DividerItemDecoration import com.bumptech.glide.Glide import com.ke.hs_tracker.module.databinding.ModuleActivityZoneCardsBinding import com.ke.hs_tracker.module.databinding.ModuleItemZoneCardBinding import com.ke.hs_tracker.module.entity.ZoneCard import com.ke.mvvm.base.ui.BaseViewBindingAdapter class ZoneCardsActivity : AppCompatActivity() { private val adapter = object : BaseViewBindingAdapter() { override fun bindItem( item: ZoneCard, viewBinding: ModuleItemZoneCardBinding, viewType: Int, position: Int ) { viewBinding.apply { cost.text = item.card?.cost?.toString() ?: "" name.text = item.card?.name ?: "未知卡牌" this.position.text = item.position.toString() item.card?.id?.let { Glide.with(this@ZoneCardsActivity) .load("https://art.hearthstonejson.com/v1/tiles/${it}.png") .into(imageTile) } } } override fun createViewBinding( inflater: LayoutInflater, parent: ViewGroup, viewType: Int ): ModuleItemZoneCardBinding { return ModuleItemZoneCardBinding.inflate(inflater, parent, false) } } private lateinit var binding: ModuleActivityZoneCardsBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityZoneCardsBinding.inflate(layoutInflater) setContentView(binding.root) binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration( DividerItemDecoration( this, DividerItemDecoration.VERTICAL ) ) binding.toolbar.setNavigationOnClickListener { onBackPressed() } val zoneCardList = intent.getParcelableArrayListExtra(EXTRA_KEY_ZONE_CARD_LIST) adapter.setList(zoneCardList?.sortedBy { it.position }) } companion object { const val EXTRA_KEY_ZONE_CARD_LIST = "EXTRA_KEY_ZONE_CARD_LIST" } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ListModeFragment.kt ================================================ package com.ke.hs_tracker.module.ui.zoneevents import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.DividerItemDecoration import com.hi.dhl.binding.viewbind import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleFragmentListModeBinding import com.ke.hs_tracker.module.databinding.ModuleItemZoneEventListModeBinding import com.ke.hs_tracker.module.entity.Zone import com.ke.hs_tracker.module.entity.ZoneCard import com.ke.mvvm.base.ui.BaseViewBindingAdapter import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint internal class ListModeFragment : Fragment() { private val onChipClickListener = View.OnClickListener { (it.tag as? List)?.apply { if (isNotEmpty()) { (activity as? ZoneEventsActivity)?.toZoneCardsActivity(this) } } } private val adapter by lazy { object : BaseViewBindingAdapter() { override fun bindItem( item: GameCardCollections, viewBinding: ModuleItemZoneEventListModeBinding, viewType: Int, position: Int ) { viewBinding.apply { userDeck.text = "玩家牌库 ${item.userDeckCardList.size}" userHand.text = "玩家手牌 ${item.userHandCardList.size}" userPlay.text = "玩家战场 ${item.userPlayCardList.size}" userGraveyard.text = "玩家墓地 ${item.userGraveyardCardList.size}" userSecret.text = "玩家奥秘 ${item.userSecretCardList.size}" opponentDeck.text = "对手牌库 ${item.opponentDeckCardList.size}" opponentHand.text = "对手手牌 ${item.opponentHandCardList.size}" opponentPlay.text = "对手战场 ${item.opponentPlayCardList.size}" opponentGraveyard.text = "对手墓地 ${item.opponentGraveyardCardList.size}" opponentSecret.text = "对手奥秘 ${item.opponentSecretCardList.size}" userDeck.setOnClickListener(onChipClickListener) userHand.setOnClickListener(onChipClickListener) userPlay.setOnClickListener(onChipClickListener) userGraveyard.setOnClickListener(onChipClickListener) userSecret.setOnClickListener(onChipClickListener) opponentDeck.setOnClickListener(onChipClickListener) opponentHand.setOnClickListener(onChipClickListener) opponentPlay.setOnClickListener(onChipClickListener) opponentGraveyard.setOnClickListener(onChipClickListener) opponentSecret.setOnClickListener(onChipClickListener) userDeck.tag = item.userDeckCardList userHand.tag = item.userHandCardList userPlay.tag = item.userPlayCardList userGraveyard.tag = item.userGraveyardCardList userSecret.tag = item.userSecretCardList opponentDeck.tag = item.opponentDeckCardList opponentHand.tag = item.opponentHandCardList opponentPlay.tag = item.opponentPlayCardList opponentGraveyard.tag = item.opponentGraveyardCardList opponentSecret.tag = item.opponentSecretCardList // userDeck.setOnClickListener { // if (item.userDeckCardList.isNotEmpty()) { // (activity as? ZoneEventsActivity)?.toZoneCardsActivity(item.userDeckCardList) // } // } } } override fun createViewBinding( inflater: LayoutInflater, parent: ViewGroup, viewType: Int ): ModuleItemZoneEventListModeBinding { return ModuleItemZoneEventListModeBinding.inflate(inflater, parent, false) } } } private val binding: ModuleFragmentListModeBinding by viewbind() private val zoneEventsViewModel: ZoneEventsViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration( DividerItemDecoration( requireContext(), DividerItemDecoration.VERTICAL ) ) launchAndRepeatWithViewLifecycle { zoneEventsViewModel.collectionsList.collect { adapter.setList(it) } } } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ZoneEventsActivity.kt ================================================ package com.ke.hs_tracker.module.ui.zoneevents import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.os.Parcelable import androidx.activity.viewModels import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter import com.ke.hs_tracker.module.R import com.ke.hs_tracker.module.databinding.ModuleActivityZoneEventsBinding import com.ke.hs_tracker.module.entity.ZoneCard import com.ke.hs_tracker.module.ui.common.LoadingFragment import com.ke.hs_tracker.module.ui.zonecards.ZoneCardsActivity import com.ke.mvvm.base.ui.FragmentViewPager2Adapter import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect @AndroidEntryPoint class ZoneEventsActivity : AppCompatActivity() { private val zoneEventsViewModel: ZoneEventsViewModel by viewModels() private lateinit var binding: ModuleActivityZoneEventsBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ModuleActivityZoneEventsBinding.inflate(layoutInflater) setContentView(binding.root) binding.toolbar.setNavigationOnClickListener { onBackPressed() } val fragmentList = listOf(LoadingFragment(), ListModeFragment(), ListModeFragment()) //禁止左右滑动 binding.viewPager.isUserInputEnabled = false binding.viewPager.adapter = object : FragmentStateAdapter(this) { override fun getItemCount(): Int { return fragmentList.size } override fun createFragment(position: Int): Fragment { return fragmentList[position] } } launchAndRepeatWithViewLifecycle { zoneEventsViewModel.currentFragmentIndex.collect { binding.toggle.isVisible = it != 0 binding.viewPager.currentItem = it } } } internal fun toZoneCardsActivity(list: List) { val intent = Intent(this, ZoneCardsActivity::class.java) intent.putParcelableArrayListExtra( ZoneCardsActivity.EXTRA_KEY_ZONE_CARD_LIST, arrayListOf().apply { addAll(list) }) startActivity(intent) } companion object { const val EXTRA_KEY_ID = "EXTRA_KEY_ID" } } ================================================ FILE: module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ZoneEventsViewModel.kt ================================================ package com.ke.hs_tracker.module.ui.zoneevents import android.os.Parcel import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ke.hs_tracker.module.db.CardDao import com.ke.hs_tracker.module.db.ZonePositionChangedEvent import com.ke.hs_tracker.module.db.ZonePositionChangedEventDao import com.ke.hs_tracker.module.entity.Card import com.ke.hs_tracker.module.entity.Zone import com.ke.hs_tracker.module.entity.ZoneCard import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import javax.inject.Inject @HiltViewModel class ZoneEventsViewModel @Inject constructor( private val zonePositionChangedEventDao: ZonePositionChangedEventDao, private val cardDao: CardDao, savedStateHandle: SavedStateHandle ) : ViewModel() { private val _currentFragmentIndex = MutableStateFlow(0) internal val currentFragmentIndex: StateFlow get() = _currentFragmentIndex private val gameId = savedStateHandle.get(ZoneEventsActivity.EXTRA_KEY_ID)!! private val gameCardCollectionsList = mutableListOf().apply { add(GameCardCollections()) } private val _collectionsList = MutableStateFlow>(emptyList()) internal val collectionsList: StateFlow> get() = _collectionsList init { viewModelScope.launch { withContext(Dispatchers.IO) { val allCard = cardDao.getAll() val list = zonePositionChangedEventDao.getAllByGameId(gameId) if (list.isEmpty()) { return@withContext } val stack = mutableListOf() list.forEach { if (it.entityId == stack.lastOrNull()?.entityId) { stack.add(it) } else { flushStack(stack, gameCardCollectionsList, allCard) } if (stack.isEmpty()) { stack.add(it) } } flushStack(stack, gameCardCollectionsList, allCard) _currentFragmentIndex.value = 1 _collectionsList.value = gameCardCollectionsList } } } companion object { private fun flushStack( list: MutableList, mutableList: MutableList, allCardList: List ) { val last = mutableList.last() if (list.isEmpty()) { return } when (list.size) { 1 -> { //仅改变区域 val event = list.first() mutableList.add(last.update(event, allCardList)) } 2 -> { //改变区域和位置 val first = list[0] val second = list[1] mutableList.add(last.update(first.plus(second), allCardList)) } 3 -> { val first = list[0] val second = list[1] val third = list[2] mutableList.add(last.update(first.plusPlus(second, third), allCardList)) } else -> { list.forEach { flushStack( mutableListOf(it), mutableList, allCardList ) } } } list.clear() } } } @Parcelize internal data class GameCardCollections( private val _userDeckCardList: MutableList = mutableListOf(), private val _opponentDeckCardList: MutableList = mutableListOf(), private val _userHandCardList: MutableList = mutableListOf(), private val _opponentHandCardList: MutableList = mutableListOf(), private val _userPlayCardList: MutableList = mutableListOf(), private val _opponentPlayCardList: MutableList = mutableListOf(), private val _userGraveyardCardList: MutableList = mutableListOf(), private val _opponentGraveyardCardList: MutableList = mutableListOf(), private val _userSecretCardList: MutableList = mutableListOf(), private val _opponentSecretCardList: MutableList = mutableListOf(), ) : Parcelable { /** * 深拷贝 */ private fun deepClone(): GameCardCollections { var parcel: Parcel? = null try { parcel = Parcel.obtain() parcel.writeParcelable(this, 0) parcel.setDataPosition(0) return parcel.readParcelable(this.javaClass.classLoader)!! } finally { parcel?.recycle() } } val userDeckCardList: List get() = _userDeckCardList val opponentDeckCardList: List get() = _opponentDeckCardList val userHandCardList: List get() = _userHandCardList val opponentHandCardList: List get() = _opponentHandCardList val userPlayCardList: List get() = _userPlayCardList val opponentPlayCardList: List get() = _opponentPlayCardList val userGraveyardCardList: List get() = _userGraveyardCardList val opponentGraveyardCardList: List get() = _opponentGraveyardCardList val userSecretCardList: List get() = _userSecretCardList val opponentSecretCardList: List get() = _opponentSecretCardList fun update(event: ZonePositionChangedEvent, allCardList: List): GameCardCollections { val copy = deepClone() val oldList = when (event.currentZone) { Zone.Play -> if (event.isUser) copy._userPlayCardList else copy._opponentPlayCardList Zone.Deck -> if (event.isUser) copy._userDeckCardList else copy._opponentDeckCardList Zone.Graveyard -> if (event.isUser) copy._userGraveyardCardList else copy._opponentGraveyardCardList Zone.Hand -> if (event.isUser) copy._userHandCardList else copy._opponentHandCardList Zone.Secret -> if (event.isUser) copy._userSecretCardList else copy._opponentSecretCardList Zone.SetAside, Zone.RemovedFromGame -> null else -> throw RuntimeException("oldList 不支持的区域类型 ${event.currentZone}") } val newList = when (event.newZone) { Zone.Play -> if (event.isUser) copy._userPlayCardList else copy._opponentPlayCardList Zone.Deck -> if (event.isUser) copy._userDeckCardList else copy._opponentDeckCardList Zone.Graveyard -> if (event.isUser) copy._userGraveyardCardList else copy._opponentGraveyardCardList Zone.Hand -> if (event.isUser) copy._userHandCardList else copy._opponentHandCardList Zone.Secret -> if (event.isUser) copy._userSecretCardList else copy._opponentSecretCardList //移除掉 Zone.SetAside, Zone.RemovedFromGame -> null else -> { throw RuntimeException("newList 不支持的区域类型 ${event.newZone}") } } if (event.currentZone == event.newZone) { addCardToList(newList, allCardList, event) } else { oldList?.removeAll { it.entityId == event.entityId } addCardToList(newList, allCardList, event) } return copy } private fun addCardToList( newList: MutableList?, allCardList: List, event: ZonePositionChangedEvent ) { val target = newList ?: emptyList() if (target.find { it.entityId == event.entityId } != null) { //存在的话就不能插入 return } newList?.add( ZoneCard( allCardList.find { it.id == event.cardId }, event.entityId, event.newPosition ) ) } } ================================================ FILE: module/src/main/res/color/module_game_state.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_arrow_back_white_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_clear_black_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_clear_red_500_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_done_black_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_done_green_500_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_done_white_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_drag_handle_white_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_keyboard_arrow_right_grey_500_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_pie_chart_white_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_play_arrow_white_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_settings_white_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_sync_white_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable/module_baseline_zoom_out_map_white_24dp.xml ================================================ ================================================ FILE: module/src/main/res/drawable-xxxhdpi/module_bg_splash.xml ================================================ ================================================ FILE: module/src/main/res/layout/module_activity_class_battle_detail.xml ================================================ ================================================ FILE: module/src/main/res/layout/module_activity_create_record.xml ================================================