├── mvi-core ├── consumer-rules.pro ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── zj │ │ └── mvi │ │ └── core │ │ ├── SingleLiveEvents.kt │ │ ├── MVIExt.kt │ │ ├── LiveEvents.kt │ │ └── MVIFlowExt.kt ├── proguard-rules.pro └── build.gradle ├── app ├── .gitignore └── src │ ├── main │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ ├── drawable │ │ │ ├── ic_add.png │ │ │ ├── ic_buy.png │ │ │ ├── ic_dau.png │ │ │ ├── ic_del.png │ │ │ ├── ic_mc.png │ │ │ ├── ic_mod.png │ │ │ ├── ic_back.png │ │ │ ├── ic_beta.png │ │ │ ├── ic_close.png │ │ │ ├── ic_down.png │ │ │ ├── ic_money.png │ │ │ ├── ic_point.png │ │ │ ├── ic_sale.png │ │ │ ├── ic_show.png │ │ │ ├── ic_star.png │ │ │ ├── ic_total.png │ │ │ ├── ic_user.png │ │ │ ├── loading.gif │ │ │ ├── ic_analyze.png │ │ │ ├── ic_calendar.png │ │ │ ├── ic_comment.png │ │ │ ├── ic_correct.png │ │ │ ├── ic_dev1_1.png │ │ │ ├── ic_dev1_2.png │ │ │ ├── ic_dev1_3.png │ │ │ ├── ic_dev1_4.png │ │ │ ├── ic_dev2_1.png │ │ │ ├── ic_dev2_2.png │ │ │ ├── ic_dev2_3.png │ │ │ ├── ic_dev2_4.png │ │ │ ├── ic_dev3_1.png │ │ │ ├── ic_dev3_2.png │ │ │ ├── ic_dev3_3.png │ │ │ ├── ic_dev3_4.png │ │ │ ├── ic_dev4_1.png │ │ │ ├── ic_dev4_2.png │ │ │ ├── ic_dev4_3.png │ │ │ ├── ic_dev4_4.png │ │ │ ├── ic_dev5_1.png │ │ │ ├── ic_dev5_10.png │ │ │ ├── ic_dev5_2.png │ │ │ ├── ic_dev5_3.png │ │ │ ├── ic_dev5_4.png │ │ │ ├── ic_dev5_5.png │ │ │ ├── ic_dev5_6.png │ │ │ ├── ic_dev5_7.png │ │ │ ├── ic_dev5_8.png │ │ │ ├── ic_dev5_9.png │ │ │ ├── ic_diamond.png │ │ │ ├── ic_download.png │ │ │ ├── ic_emerald.png │ │ │ ├── ic_feedback.png │ │ │ ├── ic_filter.png │ │ │ ├── ic_lisence.png │ │ │ ├── ic_no_reply.png │ │ │ ├── ic_no_show.png │ │ │ ├── ic_notice.png │ │ │ ├── ic_profit.png │ │ │ ├── ic_refresh.png │ │ │ ├── ic_replied.png │ │ │ ├── ic_search.png │ │ │ ├── ic_selected.png │ │ │ ├── ic_setting.png │ │ │ ├── ic_unselect.png │ │ │ ├── img_avatar.png │ │ │ ├── img_update.png │ │ │ ├── ic_bar_chart.png │ │ │ ├── ic_line_chart.png │ │ │ ├── img_login_bg.png │ │ │ ├── ic_back_to_top.png │ │ │ ├── ic_comment_line.png │ │ │ ├── ic_diamond_line.png │ │ │ ├── ic_filter_used.png │ │ │ ├── ic_open_sources.png │ │ │ ├── ic_incentive_coin.png │ │ │ └── ic_launcher_foreground.xml │ │ ├── font │ │ │ └── minecraft_ae.ttf │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── xml │ │ │ ├── file_paths.xml │ │ │ ├── network_security_config.xml │ │ │ ├── backup_rules.xml │ │ │ └── data_extraction_rules.xml │ │ ├── values-zh │ │ │ └── strings.xml │ │ └── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ ├── java │ │ └── com │ │ │ └── lemon │ │ │ └── mcdevmanager │ │ │ ├── data │ │ │ ├── common │ │ │ │ ├── ChartData.kt │ │ │ │ ├── NetworkLink.kt │ │ │ │ ├── JSONConverter.kt │ │ │ │ ├── CookiesStore.kt │ │ │ │ └── ConstantValue.kt │ │ │ ├── netease │ │ │ │ ├── feedback │ │ │ │ │ ├── ReplyBean.kt │ │ │ │ │ └── FeedbackBean.kt │ │ │ │ ├── developerFeedback │ │ │ │ │ ├── DeveloperFeedbackResponseBean.kt │ │ │ │ │ └── DeveloperFeedbackBean.kt │ │ │ │ ├── user │ │ │ │ │ ├── UserInfoBean.kt │ │ │ │ │ ├── LevelInfoBean.kt │ │ │ │ │ └── OverviewBean.kt │ │ │ │ ├── income │ │ │ │ │ ├── IncentiveBean.kt │ │ │ │ │ ├── ApplyIncomeBean.kt │ │ │ │ │ ├── IncomeDetailBean.kt │ │ │ │ │ └── ApplyIncomeDetailBean.kt │ │ │ │ ├── comment │ │ │ │ │ └── CommentBean.kt │ │ │ │ ├── login │ │ │ │ │ ├── LoginResponeBean.kt │ │ │ │ │ └── LoginRequestBean.kt │ │ │ │ └── resource │ │ │ │ │ └── ResourceBean.kt │ │ │ ├── global │ │ │ │ └── AppContext.kt │ │ │ ├── database │ │ │ │ ├── database │ │ │ │ │ ├── GlobalDataBase.kt │ │ │ │ │ └── AppDataBase.kt │ │ │ │ ├── entities │ │ │ │ │ ├── UserEntity.kt │ │ │ │ │ ├── ResourcesEntity.kt │ │ │ │ │ ├── AnalyzeEntity.kt │ │ │ │ │ └── OverviewEntity.kt │ │ │ │ └── dao │ │ │ │ │ ├── UserDao.kt │ │ │ │ │ └── InfoDao.kt │ │ │ ├── github │ │ │ │ └── update │ │ │ │ │ └── LatestReleaseBean.kt │ │ │ ├── repository │ │ │ │ ├── UpdateRepository.kt │ │ │ │ ├── MainRepository.kt │ │ │ │ ├── CommentRepository.kt │ │ │ │ ├── DeveloperFeedbackRepository.kt │ │ │ │ ├── FeedbackRepository.kt │ │ │ │ ├── LoginRepository.kt │ │ │ │ ├── IncomeRepository.kt │ │ │ │ └── RealtimeProfitRepository.kt │ │ │ └── CommonInterceptor.kt │ │ │ ├── ui │ │ │ ├── theme │ │ │ │ ├── Dimension.kt │ │ │ │ ├── Type.kt │ │ │ │ └── Color.kt │ │ │ ├── page │ │ │ │ └── OneClickPriceFeedbackPage.kt │ │ │ ├── widget │ │ │ │ ├── DividedLine.kt │ │ │ │ ├── ModalBackgroundWidget.kt │ │ │ │ ├── AppSnackbar.kt │ │ │ │ ├── LoginOutlineTextField.kt │ │ │ │ ├── FABPositionWidget.kt │ │ │ │ ├── NavigationItem.kt │ │ │ │ ├── TipsCard.kt │ │ │ │ ├── SelectableItem.kt │ │ │ │ ├── FunctionCard.kt │ │ │ │ ├── LoadingShimmerWidget.kt │ │ │ │ ├── AppLoadingWidget.kt │ │ │ │ └── FlowTabWidget.kt │ │ │ └── base │ │ │ │ └── BasePage.kt │ │ │ ├── utils │ │ │ ├── FunExt.kt │ │ │ ├── ReLoginUtils.kt │ │ │ ├── RepositoryUtils.kt │ │ │ ├── StaticUtils.kt │ │ │ └── FileUtils.kt │ │ │ ├── api │ │ │ ├── DownloadApi.kt │ │ │ ├── GithubUpdateApi.kt │ │ │ ├── CommentApi.kt │ │ │ ├── DeveloperFeedbackApi.kt │ │ │ ├── LoginApi.kt │ │ │ ├── InfoApi.kt │ │ │ ├── FeedbackApi.kt │ │ │ ├── IncomeApi.kt │ │ │ └── AnalyzeApi.kt │ │ │ ├── viewModel │ │ │ ├── SplashViewModel.kt │ │ │ └── IncentiveViewModel.kt │ │ │ └── MainActivity.kt │ └── AndroidManifest.xml │ └── androidTest │ └── java │ └── com │ └── lemon │ └── mcdevmanager │ └── ExampleInstrumentedTest.kt ├── logger ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── orhanobut │ │ └── logger │ │ ├── FormatStrategy.kt │ │ ├── LogcatLogStrategy.kt │ │ ├── LogStrategy.kt │ │ ├── DiskLogAdapter.kt │ │ ├── AndroidLogAdapter.kt │ │ ├── LogAdapter.kt │ │ ├── Printer.kt │ │ ├── MyDiskLogStrategy.kt │ │ ├── DiskLogStrategy.kt │ │ └── CsvFormatStrategy.kt └── build.gradle ├── key_store.jks ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitignore ├── settings.gradle.kts ├── gradle.properties └── gradlew.bat /mvi-core/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | /src/test/ -------------------------------------------------------------------------------- /logger/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test/ 3 | /src/androidTest/ 4 | -------------------------------------------------------------------------------- /mvi-core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/test/ 3 | /src/androidTest/ 4 | -------------------------------------------------------------------------------- /logger/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key_store.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/key_store.jks -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MCDevManager 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_add.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_buy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_buy.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dau.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dau.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_del.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_del.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_mc.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_mod.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_beta.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_down.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_money.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_money.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_point.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_sale.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_show.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_star.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_total.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_total.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/loading.gif -------------------------------------------------------------------------------- /app/src/main/res/font/minecraft_ae.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/font/minecraft_ae.ttf -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_analyze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_analyze.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_calendar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_comment.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_correct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_correct.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev1_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev1_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev1_2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev1_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev1_3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev1_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev1_4.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev2_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev2_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev2_2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev2_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev2_3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev2_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev2_4.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev3_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev3_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev3_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev3_2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev3_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev3_3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev3_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev3_4.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev4_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev4_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev4_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev4_2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev4_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev4_3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev4_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev4_4.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_10.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_4.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_5.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_6.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_7.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_8.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dev5_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_dev5_9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_diamond.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_download.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_emerald.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_emerald.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_feedback.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_filter.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lisence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_lisence.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_no_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_no_reply.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_no_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_no_show.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_notice.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_profit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_profit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_refresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_replied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_replied.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_selected.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_setting.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_unselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_unselect.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/img_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/img_avatar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/img_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/img_update.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bar_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_bar_chart.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_line_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_line_chart.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/img_login_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/img_login_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back_to_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_back_to_top.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_comment_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_comment_line.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_diamond_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_diamond_line.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter_used.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_filter_used.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open_sources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_open_sources.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_incentive_coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/drawable/ic_incentive_coin.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MC开发者管理器 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitterLemonn/MCDevManager/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/common/ChartData.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.common 2 | 3 | data class LineChartData( 4 | val label: String, 5 | val data: List 6 | ) -------------------------------------------------------------------------------- /mvi-core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/theme/Dimension.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.theme 2 | 3 | import androidx.compose.ui.unit.dp 4 | import androidx.compose.ui.unit.sp 5 | 6 | val HeaderHeight = 36.dp 7 | 8 | 9 | val TitleFontSize = 14.sp -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/feedback/ReplyBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.feedback 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ReplyBean( 7 | val content: String 8 | ) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jun 17 09:49:57 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | mcdev.webapp.163.com 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/common/NetworkLink.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.common 2 | 3 | const val NETEASE_LOGIN_LINK = "https://dl.reg.163.com/" 4 | const val NETEASE_MC_DEV_LINK = "https://mc-launcher.webapp.163.com/" 5 | 6 | const val GITHUB_RESTFUL_LINK = "https://api.github.com/repos/" -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/FormatStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | /** 4 | * Used to determine how messages should be printed or saved. 5 | * 6 | * @see PrettyFormatStrategy 7 | * 8 | * @see CsvFormatStrategy 9 | */ 10 | interface FormatStrategy { 11 | fun log(priority: Int, tag: String?, message: String) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/developerFeedback/DeveloperFeedbackResponseBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.developerFeedback 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class DeveloperFeedbackResponseBean( 8 | @SerialName("feedback_id") 9 | val feedbackId: String 10 | ) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/global/AppContext.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.global 2 | 3 | import com.lemon.mcdevmanager.data.netease.user.UserInfoBean 4 | 5 | object AppContext { 6 | var curUserInfo: UserInfoBean? = null 7 | 8 | val cookiesStore = HashMap() 9 | var nowNickname = "UNKNOWN" 10 | 11 | val accountList = mutableListOf() 12 | var logDirPath = "" 13 | } -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/common/JSONConverter.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.common 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.json.Json 5 | 6 | @OptIn(ExperimentalSerializationApi::class) 7 | val JSONConverter = Json { 8 | // 忽略实体类中不存在的字段 9 | ignoreUnknownKeys = true 10 | // 编码实体类默认值 11 | encodeDefaults = true 12 | // 忽略json空值 13 | coerceInputValues = true 14 | // 忽略实体类空值 15 | explicitNulls = false 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /.idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | /.idea/inspectionProfiles/Project_Default.xml 18 | /app/release/app-release.apk 19 | /app/release/baselineProfiles/0/app-release.dm 20 | /app/release/baselineProfiles/1/app-release.dm 21 | /app/release/output-metadata.json 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/utils/FunExt.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.utils 2 | 3 | fun String.dumpAndGetCookiesValue(key: String): String? { 4 | val cookies = this.split(";") 5 | for (cookie in cookies) { 6 | val pair = cookie.split("=") 7 | if (pair.size == 2 && pair[0].trim() == key) { 8 | return pair[1].trim() 9 | } 10 | } 11 | return null 12 | } 13 | 14 | fun String.isValidCookiesStr(): Boolean { 15 | return this.isNotEmpty() && this.contains("=") && this.contains(";") 16 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/database/database/GlobalDataBase.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.database.database 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | 7 | class GlobalDataBase : Application() { 8 | companion object { 9 | lateinit var database: AppDataBase 10 | } 11 | 12 | override fun onCreate() { 13 | super.onCreate() 14 | // 初始化数据库 15 | database = AppDataBase.getInstance(applicationContext) 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/database/entities/UserEntity.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.database.entities 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import kotlinx.serialization.Serializable 7 | 8 | @Entity 9 | @Serializable 10 | data class UserEntity( 11 | @PrimaryKey(autoGenerate = true) val id: Int = 0, 12 | @ColumnInfo val nickname: String, 13 | @ColumnInfo val cookie: String, 14 | @ColumnInfo val username: String, 15 | @ColumnInfo val password: String 16 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/developerFeedback/DeveloperFeedbackBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.developerFeedback 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class DeveloperFeedbackBean( 8 | @SerialName("feedback_type") 9 | val feedbackType: String, 10 | @SerialName("function_type") 11 | val functionType: String, 12 | @SerialName("desc") 13 | val content: String, 14 | val contact: String, 15 | @SerialName("extra_list") 16 | val extraList: List = emptyList() 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/database/entities/ResourcesEntity.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.database.entities 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.lemon.mcdevmanager.data.netease.resource.ResourceBean 7 | import kotlinx.serialization.Serializable 8 | 9 | @Entity 10 | @Serializable 11 | data class ResourcesEntity( 12 | @PrimaryKey(autoGenerate = true) private val _id: Int = 0, 13 | @ColumnInfo val nickname: String = "", 14 | @ColumnInfo val resList: List, 15 | @ColumnInfo val resCount: Int = resList.count() 16 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/page/OneClickPriceFeedbackPage.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.page 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.lifecycle.viewmodel.compose.viewModel 5 | import androidx.navigation.NavController 6 | import androidx.navigation.compose.rememberNavController 7 | import com.lemon.mcdevmanager.viewModel.OneClickPriceFeedbackViewModel 8 | 9 | @Composable 10 | fun OneClickPriceFeedbackPage( 11 | navController: NavController = rememberNavController(), 12 | showToast: (String, String) -> Unit = { _, _ -> }, 13 | viewModel: OneClickPriceFeedbackViewModel = viewModel() 14 | ) { 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/utils/ReLoginUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.utils 2 | 3 | import com.lemon.mcdevmanager.data.database.database.GlobalDataBase 4 | import com.lemon.mcdevmanager.data.global.AppContext 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | 8 | suspend fun logout(accountName: String) { 9 | AppContext.accountList.remove(accountName) 10 | AppContext.cookiesStore.remove(accountName) 11 | withContext(Dispatchers.IO) { 12 | GlobalDataBase.database.infoDao().deleteOverviewByNickname(accountName) 13 | GlobalDataBase.database.userDao().deleteUserByNickname(accountName) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/user/UserInfoBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.user 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class UserInfoBean( 8 | val exp: Int, 9 | val level: Int, 10 | @SerialName("head_img") 11 | val headImg: String? = null, 12 | val nickname: String, 13 | val income: String, 14 | @SerialName("onsale_item_count") 15 | val onSaleItemCount: Int, 16 | @SerialName("cur_month_incentive_fund") 17 | val curMonthIncentiveFund: Double, 18 | @SerialName("unextract_income") 19 | val unExtractIncome: String 20 | ) -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/LogcatLogStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | import android.util.Log 4 | 5 | /** 6 | * LogCat implementation for [LogStrategy] 7 | * 8 | * This simply prints out all logs to Logcat by using standard [Log] class. 9 | */ 10 | class LogcatLogStrategy : LogStrategy { 11 | override fun log(priority: Int, tag: String?, message: String) { 12 | var tags = tag 13 | Utils.checkNotNull(message) 14 | 15 | if (tags == null) { 16 | tags = DEFAULT_TAG 17 | } 18 | 19 | Log.println(priority, tags, message) 20 | } 21 | 22 | companion object { 23 | const val DEFAULT_TAG: String = "NO_TAG" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/LogStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | /** 4 | * Determines destination target for the logs such as Disk, Logcat etc. 5 | * 6 | * @see LogcatLogStrategy 7 | * 8 | * @see DiskLogStrategy 9 | */ 10 | interface LogStrategy { 11 | /** 12 | * This is invoked by Logger each time a log message is processed. 13 | * Interpret this method as last destination of the log in whole pipeline. 14 | * 15 | * @param priority is the log level e.g. DEBUG, WARNING 16 | * @param tag is the given tag for the log message. 17 | * @param message is the given message for the log message. 18 | */ 19 | fun log(priority: Int, tag: String?, message: String) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/github/update/LatestReleaseBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.github.update 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class LatestReleaseBean( 8 | @SerialName("tag_name") 9 | val tagName: String, 10 | val draft: Boolean, 11 | @SerialName("prerelease") 12 | val preRelease: Boolean, 13 | val assets: List, 14 | val body: String 15 | ) 16 | 17 | @Serializable 18 | data class AssetBean( 19 | val name: String, 20 | val size: Long, 21 | @SerialName("updated_at") 22 | val updatedAt: String, 23 | @SerialName("browser_download_url") 24 | val url: String 25 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/database/entities/AnalyzeEntity.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.database.entities 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import kotlinx.serialization.Serializable 7 | 8 | @Entity 9 | @Serializable 10 | data class AnalyzeEntity( 11 | @PrimaryKey(autoGenerate = true) val id: Int = 0, 12 | @ColumnInfo val nickname: String, 13 | @ColumnInfo val filterType: Int, 14 | @ColumnInfo val platform: String, 15 | @ColumnInfo val startDate: String, 16 | @ColumnInfo val endDate: String, 17 | @ColumnInfo val filterResourceList: String, 18 | @ColumnInfo val createTime: Long = System.currentTimeMillis() 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/income/IncentiveBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.income 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class IncentiveBean( 8 | @SerialName("activity_id") 9 | val activityId: String, 10 | @SerialName("incentive_count") 11 | val incentiveCount: Double, 12 | val month: String, 13 | val source: String, 14 | val status: Int, 15 | @SerialName("update_time") 16 | val updateTime: Int, 17 | ) 18 | 19 | @Serializable 20 | data class IncentiveListBean( 21 | @SerialName("incentive_details") 22 | val incentiveDetails: List, 23 | val count: Int 24 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/comment/CommentBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.comment 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class CommentBean( 8 | @SerialName("comment_tag") 9 | val commentTag: String, 10 | val iid: String, 11 | val nickname: String, 12 | @SerialName("publish_time") 13 | val publishTime: Long, 14 | @SerialName("res_name") 15 | val resName: String, 16 | val stars: String, 17 | val uid: String, 18 | @SerialName("user_comment") 19 | val userComment: String 20 | ) 21 | 22 | @Serializable 23 | data class CommentList( 24 | val count: Int, 25 | val data: List 26 | ) -------------------------------------------------------------------------------- /logger/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 33 8 | namespace 'com.orhanobut.logger' 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | } 13 | 14 | kotlinOptions { 15 | jvmTarget = "1.8" 16 | } 17 | 18 | lintOptions { 19 | textReport true 20 | textOutput 'stdout' 21 | } 22 | 23 | testOptions { 24 | unitTests.returnDefaultValues = true 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation 'androidx.annotation:annotation:1.0.0' 30 | 31 | implementation 'junit:junit:4.12' 32 | implementation 'com.google.truth:truth:0.28' 33 | implementation "org.mockito:mockito-core:2.8.9" 34 | implementation "org.json:json:20160810" 35 | } 36 | 37 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | maven { url = uri("https://jitpack.io") } 13 | } 14 | } 15 | dependencyResolutionManagement { 16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 17 | repositories { 18 | google() 19 | mavenCentral() 20 | maven { url = uri("https://jitpack.io") } 21 | } 22 | } 23 | 24 | rootProject.name = "MCDevManager" 25 | include(":app") 26 | include(":mvi-core") 27 | include (":logger") 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/database/dao/UserDao.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.lemon.mcdevmanager.data.database.entities.UserEntity 8 | 9 | @Dao 10 | interface UserDao { 11 | 12 | @Query("SELECT * FROM userEntity WHERE nickname = :nickname") 13 | fun getUserByNickname(nickname: String): UserEntity? 14 | 15 | @Query("SELECT * FROM userEntity") 16 | fun getAllUsers(): List 17 | 18 | @Insert(onConflict = OnConflictStrategy.REPLACE) 19 | fun updateUser(user: UserEntity) 20 | 21 | @Query("DELETE FROM userEntity WHERE nickname = :nickname") 22 | fun deleteUserByNickname(nickname: String) 23 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/lemon/mcdevmanager/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.lemon.mcdevmanager", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /mvi-core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/DividedLine.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import com.lemon.mcdevmanager.ui.theme.AppTheme 12 | 13 | @Composable 14 | fun DividedLine( 15 | modifier: Modifier = Modifier 16 | ) { 17 | Box( 18 | modifier = Modifier 19 | .fillMaxWidth() 20 | .padding(horizontal = 16.dp, vertical = 2.dp) 21 | .height(1.dp) 22 | .background(AppTheme.colors.hintColor.copy(alpha = 0.35f)) 23 | .then(modifier) 24 | ) 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/common/CookiesStore.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.common 2 | 3 | object CookiesStore { 4 | private val cookies = mutableMapOf() 5 | 6 | fun addCookies(list: List) { 7 | list.forEach { 8 | val cookie = it.split(";")[0] 9 | val key = cookie.split("=")[0] 10 | val value = cookie.split("=")[1] 11 | cookies[key] = value 12 | } 13 | } 14 | 15 | fun addCookie(key: String, value: String) { 16 | cookies[key] = value 17 | } 18 | 19 | fun getCookie(key: String): String? { 20 | return cookies[key] 21 | } 22 | 23 | fun getAllCookiesString(): String { 24 | if (cookies.isEmpty()) return "" 25 | return cookies.map { "${it.key}=${it.value}" }.joinToString("; ") 26 | } 27 | 28 | fun clearCookies() { 29 | cookies.clear() 30 | } 31 | } -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/DiskLogAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | /** 4 | * This is used to saves log messages to the disk. 5 | * By default it uses [CsvFormatStrategy] to translates text message into CSV format. 6 | */ 7 | open class DiskLogAdapter : LogAdapter { 8 | private val formatStrategy: FormatStrategy 9 | 10 | constructor(fileName: String, logDirPath: String) { 11 | formatStrategy = CsvFormatStrategy.newBuilder() 12 | .build(fileName, logDirPath) 13 | } 14 | 15 | 16 | constructor(formatStrategy: FormatStrategy) { 17 | this.formatStrategy = Utils.checkNotNull(formatStrategy) 18 | } 19 | 20 | override fun isLoggable(priority: Int, tag: String?): Boolean { 21 | return true 22 | } 23 | 24 | override fun log(priority: Int, tag: String?, message: String) { 25 | formatStrategy.log(priority, tag, message) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/utils/RepositoryUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.utils 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * 响应体通用类 7 | * @param T 响应体包含的数据的类型 8 | * @param status 响应体返回的状态 9 | * @param msg 响应体返回的信息 10 | */ 11 | @Serializable 12 | data class ResponseData(val status: String, val data: T? = null, val msg: String? = null) 13 | 14 | /** 15 | * 响应处理包装通用类 16 | * 成功时返回 NetworkState.Success 包含响应返回的数据 17 | * 失败时返回 NetworkState.Error 包含响应返回的错误信息 18 | * @param T 响应体包含的数据的类型 19 | */ 20 | sealed class NetworkState { 21 | data class Success(val data: T? = null, val msg: String? = null) : NetworkState() 22 | data class Error(val msg: String, val e: Exception? = null) : NetworkState() 23 | } 24 | 25 | data object CookiesExpiredException : Exception("Cookies expired, please re-login") { 26 | private fun readResolve(): Any = CookiesExpiredException 27 | } 28 | 29 | @Serializable 30 | data object NoNeedData -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## For more details on how to configure your build environment visit 2 | # http://www.gradle.org/docs/current/userguide/build_environment.html 3 | # 4 | # Specifies the JVM arguments used for the daemon process. 5 | # The setting is particularly useful for tweaking memory settings. 6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 8 | # 9 | # When configured, Gradle will run in incubating parallel mode. 10 | # This option should only be used with decoupled projects. For more details, visit 11 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 12 | # org.gradle.parallel=true 13 | #Mon Nov 04 15:48:23 CST 2024 14 | android.nonTransitiveRClass=true 15 | android.useAndroidX=true 16 | kotlin.code.style=official 17 | kotlin.native.disableCompilerDaemon=true 18 | org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8 19 | -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/AndroidLogAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | /** 4 | * Android terminal log output implementation for [LogAdapter]. 5 | * 6 | * Prints output to LogCat with pretty borders. 7 | * 8 | *
 9 |  * ┌──────────────────────────
10 |  * │ Method stack history
11 |  * ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
12 |  * │ Log message
13 |  * └──────────────────────────
14 | 
* 15 | */ 16 | class AndroidLogAdapter : LogAdapter { 17 | private val formatStrategy: FormatStrategy 18 | 19 | constructor() { 20 | this.formatStrategy = PrettyFormatStrategy.newBuilder().build() 21 | } 22 | 23 | constructor(formatStrategy: FormatStrategy) { 24 | this.formatStrategy = Utils.checkNotNull(formatStrategy) 25 | } 26 | 27 | override fun isLoggable(priority: Int, tag: String?): Boolean { 28 | return true 29 | } 30 | 31 | override fun log(priority: Int, tag: String?, message: String) { 32 | formatStrategy.log(priority, tag, message) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/LogAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | /** 4 | * Provides a common interface to emits logs through. This is a required contract for Logger. 5 | * 6 | * @see AndroidLogAdapter 7 | * 8 | * @see DiskLogAdapter 9 | */ 10 | interface LogAdapter { 11 | /** 12 | * Used to determine whether log should be printed out or not. 13 | * 14 | * @param priority is the log level e.g. DEBUG, WARNING 15 | * @param tag is the given tag for the log message 16 | * 17 | * @return is used to determine if log should printed. 18 | * If it is true, it will be printed, otherwise it'll be ignored. 19 | */ 20 | fun isLoggable(priority: Int, tag: String?): Boolean 21 | 22 | /** 23 | * Each log will use this pipeline 24 | * 25 | * @param priority is the log level e.g. DEBUG, WARNING 26 | * @param tag is the given tag for the log message. 27 | * @param message is the given message for the log message. 28 | */ 29 | fun log(priority: Int, tag: String?, message: String) 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/login/LoginResponeBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.login 2 | 3 | import com.lemon.mcdevmanager.utils.dataJsonToString 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class BaseLoginBean( 9 | val ret: Int 10 | ) 11 | 12 | @Serializable 13 | data class TicketBean( 14 | val ret: Int, 15 | val tk: String 16 | ) 17 | 18 | @Serializable 19 | data class PowerBean( 20 | val ret: Int, 21 | val pVInfo: PVInfo 22 | ) 23 | 24 | @Serializable 25 | data class PVInfo( 26 | val sid: String, 27 | val hashFunc: String, 28 | val needCheck: Boolean, 29 | val args: PVArgs, 30 | val maxTime: Int, 31 | val minTime: Int 32 | ) 33 | 34 | @Serializable 35 | data class PVArgs( 36 | val mod: String, 37 | val t: Int, 38 | val puzzle: String, 39 | val x: String 40 | ) 41 | 42 | @Serializable 43 | data class CapIdBean( 44 | val ret: Int, 45 | val capId: String, 46 | val pv: Boolean, 47 | val capFlag: Int 48 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/database/entities/OverviewEntity.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.database.entities 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity 8 | data class OverviewEntity( 9 | @PrimaryKey(autoGenerate = true) val id: Int = 0, 10 | @ColumnInfo val nickname: String, 11 | @ColumnInfo val days14AverageDiamond: Int, 12 | @ColumnInfo val days14AverageDownload: Int, 13 | @ColumnInfo val days14TotalDiamond: Int, 14 | @ColumnInfo val days14TotalDownload: Int, 15 | @ColumnInfo val lastMonthDiamond: Int, 16 | @ColumnInfo val lastMonthDownload: Int, 17 | @ColumnInfo val thisMonthDiamond: Int, 18 | @ColumnInfo val thisMonthDownload: Int, 19 | @ColumnInfo val yesterdayDiamond: Int, 20 | @ColumnInfo val yesterdayDownload: Int, 21 | @ColumnInfo val lastMonthProfit: String = "0.00", 22 | @ColumnInfo val lastMonthTax: String = "0.00", 23 | @ColumnInfo val thisMonthProfit: String = "0.00", 24 | @ColumnInfo val thisMonthTax: String = "0.00", 25 | @ColumnInfo val timestamp: Long = System.currentTimeMillis() 26 | ) 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/Printer.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | /** 4 | * A proxy interface to enable additional operations. 5 | * Contains all possible Log message usages. 6 | */ 7 | interface Printer { 8 | fun addAdapter(adapter: LogAdapter) 9 | 10 | fun t(tag: String?): Printer 11 | 12 | fun d(message: String, vararg args: Any?) 13 | 14 | fun d(`object`: Any?) 15 | 16 | fun e(message: String, vararg args: Any?) 17 | 18 | fun e(throwable: Throwable?, message: String, vararg args: Any?) 19 | 20 | fun w(message: String, vararg args: Any?) 21 | 22 | fun i(message: String, vararg args: Any?) 23 | 24 | fun v(message: String, vararg args: Any?) 25 | 26 | fun n(message: String, vararg args: Any?) 27 | 28 | fun wtf(message: String, vararg args: Any?) 29 | 30 | /** 31 | * Formats the given json content and print it 32 | */ 33 | fun json(json: String?) 34 | 35 | /** 36 | * Formats the given xml content and print it 37 | */ 38 | fun xml(xml: String?) 39 | 40 | fun log(priority: Int, tag: String?, message: String?, throwable: Throwable?) 41 | 42 | fun clearLogAdapters() 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/repository/UpdateRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.repository 2 | 3 | import com.lemon.mcdevmanager.api.DownloadApi 4 | import com.lemon.mcdevmanager.api.GithubUpdateApi 5 | import com.lemon.mcdevmanager.data.github.update.LatestReleaseBean 6 | import com.lemon.mcdevmanager.utils.NetworkState 7 | import com.lemon.mcdevmanager.utils.UnifiedExceptionHandler 8 | import okhttp3.ResponseBody 9 | 10 | class UpdateRepository { 11 | companion object { 12 | @Volatile 13 | private var instance: UpdateRepository? = null 14 | fun getInstance() = instance ?: synchronized(this) { 15 | instance ?: UpdateRepository().also { instance = it } 16 | } 17 | } 18 | 19 | suspend fun getLatestRelease(): NetworkState { 20 | return UnifiedExceptionHandler.handleSuspendWithGithubData { 21 | GithubUpdateApi.create().getLatestRelease() 22 | } 23 | } 24 | 25 | fun downloadAsset( 26 | baseUrl: String, 27 | fileUrl: String 28 | ): ResponseBody? { 29 | val call = DownloadApi.create(baseUrl).downloadFile(fileUrl) 30 | return call.execute().body() 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/DownloadApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import okhttp3.OkHttpClient 6 | import okhttp3.ResponseBody 7 | import retrofit2.Call 8 | import retrofit2.Retrofit 9 | import retrofit2.http.GET 10 | import retrofit2.http.Streaming 11 | import retrofit2.http.Url 12 | import java.util.concurrent.TimeUnit 13 | 14 | interface DownloadApi { 15 | 16 | @Streaming 17 | @GET 18 | fun downloadFile(@Url fileUrl: String): Call 19 | 20 | companion object { 21 | /** 22 | * 获取接口实例用于调用对接方法 23 | * @return ServerApi 24 | */ 25 | fun create(baseUrl: String): DownloadApi { 26 | val client = OkHttpClient.Builder() 27 | .connectTimeout(15, TimeUnit.SECONDS) 28 | .readTimeout(15, TimeUnit.SECONDS) 29 | .addInterceptor(AddCookiesInterceptor()) 30 | .build() 31 | return Retrofit.Builder() 32 | .baseUrl(baseUrl) 33 | .client(client) 34 | .build() 35 | .create(DownloadApi::class.java) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/user/LevelInfoBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.user 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class LevelInfoBean( 8 | @SerialName("current_class") 9 | val currentClass: Int, 10 | @SerialName("current_level") 11 | val currentLevel: Int, 12 | @SerialName("exp_ceiling") 13 | val expCeiling: Double, 14 | @SerialName("exp_floor") 15 | val expFloor: Double, 16 | @SerialName("total_exp") 17 | val totalExp: Double, 18 | @SerialName("upgrade_class_achieve") 19 | val upgradeClassAchieve: Boolean, 20 | @SerialName("contribution_month") 21 | val contributionMonth: String, 22 | @SerialName("contribution_netgame_class") 23 | val contributionNetGameClass: Int, 24 | @SerialName("contribution_netgame_rank") 25 | val contributionNetGameRank: Int, 26 | @SerialName("contribution_netgame_score") 27 | val contributionNetGameScore: String, 28 | @SerialName("contribution_class") 29 | val contributionClass: Int, 30 | @SerialName("contribution_rank") 31 | val contributionRank: Int, 32 | @SerialName("contribution_score") 33 | val contributionScore: String 34 | ) -------------------------------------------------------------------------------- /mvi-core/src/main/java/com/zj/mvi/core/SingleLiveEvents.kt: -------------------------------------------------------------------------------- 1 | package com.zj.mvi.core 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.LifecycleOwner 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.Observer 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | /** 10 | * SingleLiveEvents 11 | * 负责处理多维度一次性Event 12 | * 比如我们在请求开始时发出ShowLoading,网络请求成功后发出DismissLoading与Toast事件 13 | * 如果我们在请求开始后回到桌面,成功后再回到App,这样有一个事件就会被覆盖,因此将所有事件通过List存储 14 | */ 15 | @Deprecated("Use LiveEvents instead") 16 | class SingleLiveEvents : MutableLiveData>() { 17 | private val pending = AtomicBoolean(false) 18 | private val eventList = mutableListOf>() 19 | 20 | @MainThread 21 | override fun observe(owner: LifecycleOwner, observer: Observer>) { 22 | super.observe(owner) { t -> 23 | if (pending.compareAndSet(true, false)) { 24 | eventList.clear() 25 | observer.onChanged(t) 26 | } 27 | } 28 | } 29 | 30 | @MainThread 31 | override fun setValue(t: List?) { 32 | pending.set(true) 33 | t?.let { 34 | eventList.add(it) 35 | } 36 | val list = eventList.flatten() 37 | super.setValue(list) 38 | } 39 | } -------------------------------------------------------------------------------- /mvi-core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 33 8 | namespace 'com.zj.mvi.core' 9 | 10 | defaultConfig { 11 | minSdk 21 12 | targetSdk 31 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = '1.8' 30 | } 31 | } 32 | 33 | dependencies { 34 | 35 | implementation 'androidx.core:core-ktx:1.7.0' 36 | implementation 'androidx.appcompat:appcompat:1.4.1' 37 | implementation 'com.google.android.material:material:1.5.0' 38 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1" 39 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1" 40 | implementation libs.androidx.lifecycle.runtime.android 41 | testImplementation 'junit:junit:4.12' 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/income/ApplyIncomeBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.income 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApplyIncomeBean( 8 | @SerialName("income_id") 9 | val incomeIds: List 10 | ) 11 | 12 | // 实时收益 13 | @Serializable 14 | data class OneResRealtimeIncomeBean( 15 | val count: Int = 0, 16 | @SerialName("total_diamonds") 17 | val totalDiamonds: Int = 0, 18 | @SerialName("total_points") 19 | val totalPoints: Int = 0, 20 | val orders: List = emptyList() 21 | ) 22 | 23 | @Serializable 24 | data class OneResRealtimeIncomeOrderBean( 25 | @SerialName("app_orderid") 26 | val appOrderId: String, 27 | @SerialName("app_uid") 28 | val appUid: String, 29 | val discount: String, 30 | val point: Int, 31 | @SerialName("point_type") 32 | val pointType: String, 33 | val price: Int, 34 | @SerialName("price_type") 35 | val priceType: String, 36 | @SerialName("product_name") 37 | val productName: String, 38 | @SerialName("purchase_limit") 39 | val purchaseLimit: Int, 40 | @SerialName("refund_status") 41 | val refundStatus: String, 42 | @SerialName("ship_time") 43 | val shipTime: String 44 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/user/OverviewBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.user 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class OverviewBean( 8 | @SerialName("day_diamond_diff") 9 | val dayDiamondDiff: Int, 10 | @SerialName("day_download_diff") 11 | val dayDownloadDiff: Int, 12 | @SerialName("days_14_average_diamond") 13 | val days14AverageDiamond: Int, 14 | @SerialName("days_14_average_download") 15 | val days14AverageDownload: Int, 16 | @SerialName("days_14_total_diamond") 17 | val days14TotalDiamond: Int, 18 | @SerialName("days_14_total_download") 19 | val days14TotalDownload: Int, 20 | @SerialName("last_month_diamond") 21 | val lastMonthDiamond: Int, 22 | @SerialName("last_month_download") 23 | val lastMonthDownload: Int, 24 | @SerialName("month_diamond_diff") 25 | val monthDiamondDiff: Int, 26 | @SerialName("month_download_diff") 27 | val monthDownloadDiff: Int, 28 | @SerialName("this_month_diamond") 29 | val thisMonthDiamond: Int, 30 | @SerialName("this_month_download") 31 | val thisMonthDownload: Int, 32 | @SerialName("yesterday_diamond") 33 | val yesterdayDiamond: Int, 34 | @SerialName("yesterday_download") 35 | val yesterdayDownload: Int 36 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/base/BasePage.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.base 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.WindowInsets 5 | import androidx.compose.foundation.layout.asPaddingValues 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.navigationBars 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.DisposableEffect 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.ui.Modifier 13 | import androidx.lifecycle.compose.LocalLifecycleOwner 14 | import com.zj.mvi.core.observeEvent 15 | import kotlinx.coroutines.Job 16 | import kotlinx.coroutines.flow.SharedFlow 17 | 18 | @Composable 19 | fun BasePage( 20 | viewEvent: SharedFlow>, 21 | onEvent: (Any?) -> Unit, 22 | content: @Composable (Modifier) -> Unit 23 | ) { 24 | var eventJob: Job? = null 25 | val lifecycleOwner = LocalLifecycleOwner.current 26 | LaunchedEffect(Unit) { 27 | eventJob = viewEvent.observeEvent(lifecycleOwner) { event -> 28 | onEvent(event) 29 | } 30 | } 31 | DisposableEffect(Unit) { 32 | onDispose { 33 | eventJob?.cancel() 34 | } 35 | } 36 | 37 | content(Modifier.padding(WindowInsets.navigationBars.asPaddingValues())) 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/ModalBackgroundWidget.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.fadeIn 5 | import androidx.compose.animation.fadeOut 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.interaction.MutableInteractionSource 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | 16 | @Composable 17 | fun ModalBackgroundWidget( 18 | modifier: Modifier = Modifier, 19 | visibility: Boolean = false, 20 | onClick: () -> Unit = {} 21 | ) { 22 | AnimatedVisibility( 23 | visible = visibility, 24 | enter = fadeIn(), 25 | exit = fadeOut() 26 | ) { 27 | Box( 28 | modifier = Modifier 29 | .fillMaxSize() 30 | .background(Color.Black.copy(alpha = 0.5f)) 31 | .clickable( 32 | indication = null, 33 | interactionSource = remember { MutableInteractionSource() }, 34 | onClick = onClick 35 | ) 36 | .then(modifier) 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/repository/MainRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.repository 2 | 3 | import com.lemon.mcdevmanager.api.InfoApi 4 | import com.lemon.mcdevmanager.data.netease.user.LevelInfoBean 5 | import com.lemon.mcdevmanager.data.netease.user.OverviewBean 6 | import com.lemon.mcdevmanager.data.netease.user.UserInfoBean 7 | import com.lemon.mcdevmanager.utils.NetworkState 8 | import com.lemon.mcdevmanager.utils.UnifiedExceptionHandler 9 | 10 | class MainRepository { 11 | companion object { 12 | @Volatile 13 | private var instance: MainRepository? = null 14 | fun getInstance() = instance ?: synchronized(this) { 15 | instance ?: MainRepository().also { instance = it } 16 | } 17 | } 18 | 19 | suspend fun getUserInfo(): NetworkState { 20 | return UnifiedExceptionHandler.handleSuspendWithCall { 21 | InfoApi.create().getUserInfo() 22 | } 23 | } 24 | 25 | suspend fun getOverview(): NetworkState { 26 | return UnifiedExceptionHandler.handleSuspendWithCall { 27 | InfoApi.create().getOverview() 28 | } 29 | } 30 | 31 | suspend fun getLevelInfo(): NetworkState { 32 | return UnifiedExceptionHandler.handleSuspendWithCall { 33 | InfoApi.create().getLevelInfo() 34 | } 35 | } 36 | 37 | fun stopAllCalls() { 38 | UnifiedExceptionHandler.stopAllCalls() 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/feedback/FeedbackBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.feedback 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class FeedbackBean( 8 | @SerialName("_id") 9 | val id: String = "0", 10 | @SerialName("commit_nickname") 11 | val commitNickname: String = "", 12 | @SerialName("commit_uid") 13 | val commitUid: String = "", 14 | val content: String = "", 15 | @SerialName("create_time") 16 | val createTime: Long = 0, 17 | @SerialName("feedback_log_file") 18 | val feedbackLogFile: String = "", 19 | @SerialName("forbid_reply") 20 | val forbidReply: Boolean = false, 21 | @SerialName("have_log_file") 22 | val haveLogFile: Boolean = false, 23 | val iid: String = "", 24 | @SerialName("pic_list") 25 | val picList: List = emptyList(), 26 | val reply: String? = null, 27 | @SerialName("res_name") 28 | val resName: String = "", 29 | val type: String = "" 30 | ) 31 | 32 | @Serializable 33 | data class FeedbackResponseBean( 34 | val data: List, 35 | val count: Int 36 | ) 37 | 38 | @Serializable 39 | data class ConflictModBean( 40 | val iid: Long? = null, 41 | val name: String 42 | ) 43 | 44 | @Serializable 45 | data class ConflictModsBean( 46 | @SerialName("item_list") 47 | val itemList: List, 48 | @SerialName("conflict_type") 49 | val conflictType: List, 50 | val detail: String? = null 51 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/AppSnackbar.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.material3.Snackbar 4 | import androidx.compose.material3.SnackbarData 5 | import androidx.compose.material3.SnackbarDuration 6 | import androidx.compose.material3.SnackbarHostState 7 | import androidx.compose.runtime.Composable 8 | import com.lemon.mcdevmanager.ui.theme.AppTheme 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.launch 11 | 12 | const val SNACK_INFO = "确定" 13 | const val SNACK_WARN = " " 14 | const val SNACK_ERROR = " " 15 | const val SNACK_SUCCESS = "OK" 16 | 17 | @Composable 18 | fun AppSnackbar( 19 | data: SnackbarData 20 | ) { 21 | Snackbar( 22 | snackbarData = data, 23 | containerColor = when (data.visuals.actionLabel) { 24 | SNACK_INFO -> AppTheme.colors.info 25 | SNACK_WARN -> AppTheme.colors.warn 26 | SNACK_ERROR -> AppTheme.colors.error 27 | SNACK_SUCCESS -> AppTheme.colors.success 28 | else -> AppTheme.colors.info 29 | } 30 | ) 31 | } 32 | 33 | 34 | fun popupSnackBar( 35 | scope: CoroutineScope, 36 | snackbarHostState: SnackbarHostState, 37 | label: String, 38 | message: String, 39 | onDismissCallback: () -> Unit = {} 40 | ) { 41 | scope.launch { 42 | snackbarHostState.showSnackbar( 43 | actionLabel = label, 44 | message = message, 45 | duration = SnackbarDuration.Short 46 | ) 47 | onDismissCallback.invoke() 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/repository/CommentRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.repository 2 | 3 | import com.lemon.mcdevmanager.api.CommentApi 4 | import com.lemon.mcdevmanager.data.common.CookiesStore 5 | import com.lemon.mcdevmanager.data.common.NETEASE_USER_COOKIE 6 | import com.lemon.mcdevmanager.data.global.AppContext 7 | import com.lemon.mcdevmanager.data.netease.comment.CommentList 8 | import com.lemon.mcdevmanager.utils.CookiesExpiredException 9 | import com.lemon.mcdevmanager.utils.NetworkState 10 | import com.lemon.mcdevmanager.utils.UnifiedExceptionHandler 11 | 12 | class CommentRepository { 13 | companion object { 14 | @Volatile 15 | private var instance: CommentRepository? = null 16 | fun getInstance() = instance ?: synchronized(this) { 17 | instance ?: CommentRepository().also { instance = it } 18 | } 19 | } 20 | 21 | suspend fun getCommentList( 22 | page: Int = 0, 23 | span: Int = 20, 24 | key: String? = null, 25 | tag: String? = null, 26 | state: Int? = null, 27 | startDate: String? = null, 28 | endDate: String? = null 29 | ): NetworkState { 30 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 31 | cookie?.let { 32 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 33 | return UnifiedExceptionHandler.handleSuspend { 34 | CommentApi.create().getCommentList( 35 | start = page * span, 36 | span = span, 37 | key = key, 38 | tag = tag, 39 | state = state, 40 | startDate = startDate, 41 | endDate = endDate 42 | ) 43 | } 44 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/GithubUpdateApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import com.lemon.mcdevmanager.data.common.GITHUB_RESTFUL_LINK 6 | import com.lemon.mcdevmanager.data.common.JSONConverter 7 | import com.lemon.mcdevmanager.data.github.update.LatestReleaseBean 8 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 9 | import okhttp3.OkHttpClient 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 12 | import retrofit2.http.GET 13 | import retrofit2.http.Path 14 | import java.util.concurrent.TimeUnit 15 | 16 | interface GithubUpdateApi { 17 | 18 | @GET("/repos/{author}/{repo}/releases/latest") 19 | suspend fun getLatestRelease( 20 | @Path("author") author: String = "BitterLemonn", 21 | @Path("repo") repo: String = "MCDevManager" 22 | ): LatestReleaseBean 23 | 24 | companion object { 25 | /** 26 | * 获取接口实例用于调用对接方法 27 | * @return ServerApi 28 | */ 29 | fun create(): GithubUpdateApi { 30 | val client = OkHttpClient.Builder() 31 | .connectTimeout(15, TimeUnit.SECONDS) 32 | .readTimeout(15, TimeUnit.SECONDS) 33 | .addInterceptor(AddCookiesInterceptor()) 34 | .addInterceptor(CommonInterceptor()) 35 | .build() 36 | return Retrofit.Builder() 37 | .baseUrl(GITHUB_RESTFUL_LINK) 38 | .addConverterFactory( 39 | JSONConverter.asConverterFactory( 40 | "application/json; charset=UTF8".toMediaTypeOrNull()!! 41 | ) 42 | ) 43 | .client(client) 44 | .build() 45 | .create(GithubUpdateApi::class.java) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/income/IncomeDetailBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.income 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class IncomeDetailBean( 8 | val count: Int = 0, 9 | val incomes: List = emptyList() 10 | ) 11 | 12 | @Serializable 13 | data class IncomeBean( 14 | @SerialName("_id") 15 | val id: String = "", 16 | @SerialName("adjust_diamond") 17 | val adjustDiamond: Int = 0, 18 | @SerialName("available_detail") 19 | val availableDetail: List = emptyList(), 20 | @SerialName("available_income") 21 | val availableIncome: String = "0.00", 22 | @SerialName("data_month") 23 | val dataMonth: String = "", 24 | @SerialName("incentive_income") 25 | val incentiveIncome: String = "0.00", 26 | val income: String = "0.00", 27 | @SerialName("op_time") 28 | val opTime: String = "", 29 | val platform: String = "pe", 30 | @SerialName("play_plan_income") 31 | val playPlanIncome: String = "0.00", 32 | @SerialName("status") 33 | private val _status: String = "", 34 | val tax: String = "0.00", 35 | @SerialName("tech_service_fee") 36 | val techServiceFee: Double = 0.0, 37 | @SerialName("total_diamond") 38 | val totalDiamond: Int = 0, 39 | @SerialName("total_usage_price") 40 | val totalUsagePrice: Double = 0.0, 41 | val type: String = "" 42 | ) { 43 | val status: String 44 | get() = if (_status == "init") "未结算" else if (_status == "fail") "结算失败" else if (_status == "applying") "结算中" else if (_status == "pay_success") "已打款" else if (_status == "pay_fail") "打款失败" else if (_status == "need_modify") "结算信息待更正" else "---" 45 | } 46 | 47 | @Serializable 48 | data class IncomeAvailableDetail( 49 | @SerialName("data_month") 50 | val dataMonth: String = "", 51 | val income: String = "0.00" 52 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/CommentApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import com.lemon.mcdevmanager.data.common.JSONConverter 6 | import com.lemon.mcdevmanager.data.common.NETEASE_MC_DEV_LINK 7 | import com.lemon.mcdevmanager.data.netease.comment.CommentList 8 | import com.lemon.mcdevmanager.utils.ResponseData 9 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 10 | import okhttp3.OkHttpClient 11 | import retrofit2.Retrofit 12 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 13 | import retrofit2.http.GET 14 | import retrofit2.http.Query 15 | import java.util.concurrent.TimeUnit 16 | 17 | interface CommentApi { 18 | 19 | @GET("/items/comment/pe/") 20 | suspend fun getCommentList( 21 | @Query("start") start: Int = 0, 22 | @Query("span") span: Int = 20, 23 | @Query("fuzzy_key") key: String? = null, 24 | @Query("comment_tag") tag: String? = null, 25 | @Query("comment_state") state: Int? = null, 26 | @Query("start_date") startDate: String? = null, 27 | @Query("end_date") endDate: String? = null 28 | ): ResponseData 29 | 30 | companion object { 31 | /** 32 | * 获取接口实例用于调用对接方法 33 | * @return CommentApi 34 | */ 35 | fun create(): CommentApi { 36 | val client = OkHttpClient.Builder().connectTimeout(15, TimeUnit.SECONDS) 37 | .readTimeout(15, TimeUnit.SECONDS).addInterceptor(AddCookiesInterceptor()) 38 | .addInterceptor(CommonInterceptor()).build() 39 | return Retrofit.Builder().baseUrl(NETEASE_MC_DEV_LINK).addConverterFactory( 40 | JSONConverter.asConverterFactory( 41 | "application/json; charset=UTF8".toMediaTypeOrNull()!! 42 | ) 43 | ).client(client).build().create(CommentApi::class.java) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/DeveloperFeedbackApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import com.lemon.mcdevmanager.data.common.JSONConverter 6 | import com.lemon.mcdevmanager.data.common.NETEASE_MC_DEV_LINK 7 | import com.lemon.mcdevmanager.data.netease.developerFeedback.DeveloperFeedbackBean 8 | import com.lemon.mcdevmanager.data.netease.developerFeedback.DeveloperFeedbackResponseBean 9 | import com.lemon.mcdevmanager.utils.ResponseData 10 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 11 | import okhttp3.OkHttpClient 12 | import retrofit2.Retrofit 13 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 14 | import retrofit2.http.Body 15 | import retrofit2.http.POST 16 | import java.util.concurrent.TimeUnit 17 | 18 | interface DeveloperFeedbackApi { 19 | @POST("/developer/feedback/add_feedback") 20 | suspend fun seedFeedback(@Body feedbackBean: DeveloperFeedbackBean): ResponseData 21 | 22 | companion object { 23 | /** 24 | * 获取接口实例用于调用对接方法 25 | * @return ServerApi 26 | */ 27 | fun create(): DeveloperFeedbackApi { 28 | val client = OkHttpClient.Builder() 29 | .connectTimeout(15, TimeUnit.SECONDS) 30 | .readTimeout(15, TimeUnit.SECONDS) 31 | .addInterceptor(AddCookiesInterceptor()) 32 | .addInterceptor(CommonInterceptor()) 33 | .build() 34 | return Retrofit.Builder() 35 | .baseUrl(NETEASE_MC_DEV_LINK) 36 | .addConverterFactory( 37 | JSONConverter.asConverterFactory( 38 | "application/json; charset=UTF8".toMediaTypeOrNull()!! 39 | ) 40 | ) 41 | .client(client) 42 | .build() 43 | .create(DeveloperFeedbackApi::class.java) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/database/dao/InfoDao.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.lemon.mcdevmanager.data.database.entities.AnalyzeEntity 8 | import com.lemon.mcdevmanager.data.database.entities.OverviewEntity 9 | 10 | @Dao 11 | interface InfoDao { 12 | 13 | @Query("SELECT * FROM overviewEntity WHERE nickname = :nickname ORDER BY timestamp DESC LIMIT 1") 14 | fun getLatestOverviewByNickname(nickname: String): OverviewEntity? 15 | 16 | @Insert(onConflict = OnConflictStrategy.REPLACE) 17 | fun insertOverview(overviewEntity: OverviewEntity) 18 | 19 | @Query("DELETE FROM overviewEntity WHERE nickname = :nickname") 20 | fun deleteOverviewByNickname(nickname: String) 21 | 22 | // 清除指定nickname除了最新的之外的所有数据 23 | @Query("DELETE FROM overviewEntity WHERE nickname = :nickname AND timestamp != (SELECT timestamp FROM overviewEntity WHERE nickname = :nickname ORDER BY timestamp DESC LIMIT 1)") 24 | fun clearCacheOverviewByNickname(nickname: String) 25 | 26 | @Query("SELECT platform FROM analyzeEntity WHERE nickname = :nickname ORDER BY createTime DESC LIMIT 1") 27 | fun getLastAnalyzePlatformByNickname(nickname: String): String? 28 | 29 | @Query("SELECT * FROM analyzeEntity WHERE nickname = :nickname AND platform = :platform ORDER BY createTime DESC LIMIT 1") 30 | fun getLastAnalyzeParamsByNicknamePlatform(nickname: String, platform: String): AnalyzeEntity? 31 | 32 | @Insert(onConflict = OnConflictStrategy.REPLACE) 33 | fun insertAnalyzeParam(analyzeEntity: AnalyzeEntity) 34 | 35 | // 清除指定nickname除了最新的之外的所有数据 保留不同platform的最新数据 36 | @Query("DELETE FROM analyzeEntity WHERE nickname = :nickname AND platform = :platform AND createTime != (SELECT createTime FROM analyzeEntity WHERE nickname = :nickname AND platform = :platform ORDER BY createTime DESC LIMIT 1)") 37 | fun clearCacheAnalyzeByNicknamePlatform(nickname: String, platform: String) 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/CommonInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data 2 | 3 | //import com.orhanobut.logger.Logger 4 | import android.text.TextUtils 5 | import com.lemon.mcdevmanager.data.common.CookiesStore 6 | import com.orhanobut.logger.Logger 7 | import okhttp3.Interceptor 8 | import okhttp3.Request 9 | import okhttp3.Response 10 | import okhttp3.ResponseBody 11 | import java.io.IOException 12 | 13 | 14 | class CommonInterceptor : Interceptor { 15 | @Throws(IOException::class) 16 | override fun intercept(chain: Interceptor.Chain): Response { 17 | val request: Request = chain.request() 18 | val t1 = System.nanoTime() 19 | 20 | request.body?.let { 21 | val buffer = okio.Buffer() 22 | it.writeTo(buffer) 23 | Logger.d("拦截器:\n发送请求至 ${request.url}\n请求头: ${request.headers}\n请求体: ${buffer.readUtf8()}") 24 | } ?: run { 25 | Logger.d("拦截器:\n发送请求至 ${request.url}\n请求头: ${request.headers}") 26 | } 27 | val response: Response = chain.proceed(request) 28 | val t2 = System.nanoTime() 29 | Logger.d("拦截器:\n收到返回 ${response.request.url}\n耗时 ${(t2 - t1) / 1e6}ms\n回复头: ${response.headers}") 30 | if (response.headers("Set-Cookie").isNotEmpty()) { 31 | val cookies = response.headers("Set-Cookie") 32 | CookiesStore.addCookies(cookies) 33 | } 34 | //查看返回数据 35 | val responseBody: ResponseBody = response.peekBody(1024 * 1024.toLong()) 36 | Logger.d("拦截器:\n返回数据: ${responseBody.string()}") 37 | return response 38 | } 39 | } 40 | 41 | class AddCookiesInterceptor : Interceptor { 42 | @Throws(IOException::class) 43 | override fun intercept(chain: Interceptor.Chain): Response { 44 | val builder = chain.request().newBuilder() 45 | //添加Cookie 46 | val cookiesStr = CookiesStore.getAllCookiesString() 47 | if (!TextUtils.isEmpty(cookiesStr)) { 48 | builder.addHeader("Cookie", cookiesStr) 49 | } 50 | return chain.proceed(builder.build()) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/repository/DeveloperFeedbackRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.repository 2 | 3 | import com.lemon.mcdevmanager.api.AnalyzeApi 4 | import com.lemon.mcdevmanager.api.DeveloperFeedbackApi 5 | import com.lemon.mcdevmanager.data.common.CookiesStore 6 | import com.lemon.mcdevmanager.data.common.NETEASE_USER_COOKIE 7 | import com.lemon.mcdevmanager.data.global.AppContext 8 | import com.lemon.mcdevmanager.data.netease.developerFeedback.DeveloperFeedbackBean 9 | import com.lemon.mcdevmanager.data.netease.developerFeedback.DeveloperFeedbackResponseBean 10 | import com.lemon.mcdevmanager.utils.CookiesExpiredException 11 | import com.lemon.mcdevmanager.utils.NetworkState 12 | import com.lemon.mcdevmanager.utils.UnifiedExceptionHandler 13 | 14 | class DeveloperFeedbackRepository { 15 | companion object { 16 | @Volatile 17 | private var instance: DeveloperFeedbackRepository? = null 18 | fun getInstance() = instance ?: synchronized(this) { 19 | instance ?: DeveloperFeedbackRepository().also { instance = it } 20 | } 21 | } 22 | 23 | suspend fun submitFeedback( 24 | content: String, 25 | contact: String, 26 | feedbackType: String, 27 | functionType: String, 28 | imgPathList: List = emptyList() 29 | ): NetworkState { 30 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 31 | cookie?.let { 32 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 33 | val feedbackBean = DeveloperFeedbackBean( 34 | feedbackType = feedbackType, 35 | functionType = functionType, 36 | content = content, 37 | contact = contact, 38 | extraList = imgPathList 39 | ) 40 | return UnifiedExceptionHandler.handleSuspend { 41 | DeveloperFeedbackApi.create().seedFeedback(feedbackBean) 42 | } 43 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFB39DDB) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink200 = Color(0xFFEF9A9A) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFFE57373) 12 | 13 | val TextDay = Color(0xFF121212) 14 | val TextNight = Color(0xFFCCCCCC) 15 | 16 | val TextWhite = Color(0xFFFFFFFF) 17 | val TextBlack = Color(0xFF000000) 18 | val Hint = Color(0xFF9E9E9E) 19 | val DividerLight = Color(0xFFE0E0E0) 20 | val DividerDark = Color(0xFF707070) 21 | 22 | val CardLight = Color(0xFFFEFEFE) 23 | val CardDark = Color(0xFF313131) 24 | 25 | val BackgroundLight = Color(0xFFF1F1F1) 26 | val BackgroundDark = Color(0xFF121212) 27 | 28 | val IconLight = Color.White 29 | val IconDark = Color(0xFFAAAAAA) 30 | 31 | 32 | val InfoLight = Color(0xFF2196F3) 33 | val InfoNight = Color(0xFF40739B) 34 | 35 | val WarnLight = Color(0xFFFFC107) 36 | val WarnNight = Color(0xFFD8A000) 37 | 38 | val SuccessLight = Color(0xFF4CAF50) 39 | val SuccessNight = Color(0xFF2E7D32) 40 | 41 | val ErrorLight = Color(0xFFFF5252) 42 | val ErrorNight = Color(0xFFC62828) 43 | 44 | val LineChartColor1Light = Color(0xFF4CAF50) 45 | val LineChartColor2Light = Color(0xFF2196F3) 46 | val LineChartColor3Light = Color(0xFFFF5252) 47 | val LineChartColor4Light = Color(0xFFFFC107) 48 | val LineChartColor5Light = Color(0xFF9C27B0) 49 | val LineChartColorsLight = listOf( 50 | LineChartColor1Light, 51 | LineChartColor2Light, 52 | LineChartColor3Light, 53 | LineChartColor4Light, 54 | LineChartColor5Light 55 | ) 56 | 57 | val LineChartColor1Dark = Color(0xFF2E7D32) 58 | val LineChartColor2Dark = Color(0xFF40739B) 59 | val LineChartColor3Dark = Color(0xFFC62828) 60 | val LineChartColor4Dark = Color(0xFFD8A000) 61 | val LineChartColor5Dark = Color(0xFF7B1FA2) 62 | val LineChartColorsDark = listOf( 63 | LineChartColor1Dark, 64 | LineChartColor2Dark, 65 | LineChartColor3Dark, 66 | LineChartColor4Dark, 67 | LineChartColor5Dark 68 | ) 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/login/LoginRequestBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.login 2 | 3 | import com.lemon.mcdevmanager.utils.getRandomTid 4 | import kotlinx.serialization.Serializable 5 | import com.lemon.mcdevmanager.data.common.pd as PD 6 | import com.lemon.mcdevmanager.data.common.pkid as PKID 7 | import com.lemon.mcdevmanager.data.common.pkht as PKHT 8 | import com.lemon.mcdevmanager.data.common.channel as CHANNEL 9 | 10 | @Serializable 11 | data class TicketRequestBean( 12 | val un: String, 13 | val pd: String = PD, 14 | val pkid: String = PKID, 15 | val channel: Int = CHANNEL, 16 | val topURL: String, 17 | val rtid: String = getRandomTid() 18 | ) 19 | 20 | @Serializable 21 | data class LoginRequestBean( 22 | val un: String, 23 | val pw: String, 24 | val pd: String = PD, 25 | val l: Int = 0, 26 | val d: Int = 10, 27 | val t: Long = System.currentTimeMillis(), 28 | val tk: String, 29 | val pwdKeyUp: Int = 1, 30 | val pkid: String = PKID, 31 | val domains: String = "", 32 | val pvParam: PVResultStrBean, 33 | val channel: Int = CHANNEL, 34 | val topURL: String, 35 | val rtid: String = getRandomTid() 36 | ) 37 | 38 | @Serializable 39 | data class GetPowerRequestBean( 40 | val pkid: String = PKID, 41 | val pd: String = PD, 42 | val un: String, 43 | val channel: Int = CHANNEL, 44 | val topURL: String, 45 | val rtid: String = getRandomTid() 46 | ) 47 | 48 | @Serializable 49 | data class GetCapIdRequestBean( 50 | val pd: String = PD, 51 | val pkid: String = PKID, 52 | val pkht: String = PKHT, 53 | val channel: Int = CHANNEL, 54 | val topURL: String, 55 | val rtid: String = getRandomTid() 56 | ) 57 | 58 | @Serializable 59 | data class EncParams( 60 | val encParams: String 61 | ) 62 | 63 | @Serializable 64 | data class PVResultStrBean( 65 | val maxTime: Int, 66 | val puzzle: String, 67 | val spendTime: Int, 68 | val runTimes: Int, 69 | val sid: String, 70 | val args: String 71 | ) 72 | 73 | @Serializable 74 | data class PVResultArgs( 75 | val x: String, 76 | val t: Int, 77 | var sign: Int = 0 78 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/LoginApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import com.lemon.mcdevmanager.data.common.JSONConverter 6 | import com.lemon.mcdevmanager.data.common.NETEASE_LOGIN_LINK 7 | import com.lemon.mcdevmanager.data.netease.login.BaseLoginBean 8 | import com.lemon.mcdevmanager.data.netease.login.CapIdBean 9 | import com.lemon.mcdevmanager.data.netease.login.EncParams 10 | import com.lemon.mcdevmanager.data.netease.login.PowerBean 11 | import com.lemon.mcdevmanager.data.netease.login.TicketBean 12 | import kotlinx.serialization.json.Json 13 | import okhttp3.MediaType 14 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 15 | import okhttp3.OkHttpClient 16 | import retrofit2.Retrofit 17 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 18 | import retrofit2.http.Body 19 | import retrofit2.http.GET 20 | import retrofit2.http.POST 21 | 22 | interface LoginApi { 23 | 24 | @POST("/dl/zj/mail/ini") 25 | suspend fun init(@Body encParams: EncParams): CapIdBean 26 | 27 | @POST("/dl/zj/mail/powGetP") 28 | suspend fun getPower(@Body encParams: EncParams): PowerBean 29 | 30 | @POST("/dl/zj/mail/gt") 31 | suspend fun getTicket(@Body encParams: EncParams): TicketBean 32 | 33 | @POST("/dl/zj/mail/l") 34 | suspend fun safeLogin(@Body encParams: EncParams): BaseLoginBean 35 | 36 | companion object { 37 | /** 38 | * 获取接口实例用于调用对接方法 39 | * @return ServerApi 40 | */ 41 | fun create(): LoginApi { 42 | val client = OkHttpClient.Builder() 43 | .addInterceptor(AddCookiesInterceptor()) 44 | .addInterceptor(CommonInterceptor()) 45 | .build() 46 | return Retrofit.Builder() 47 | .baseUrl(NETEASE_LOGIN_LINK) 48 | .addConverterFactory( 49 | JSONConverter.asConverterFactory( 50 | "application/json; charset=UTF8".toMediaTypeOrNull()!! 51 | ) 52 | ) 53 | .client(client) 54 | .build() 55 | .create(LoginApi::class.java) 56 | } 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/viewModel/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.viewModel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.lemon.mcdevmanager.data.common.LOGIN_PAGE 6 | import com.lemon.mcdevmanager.data.common.MAIN_PAGE 7 | import com.lemon.mcdevmanager.data.database.database.GlobalDataBase 8 | import com.lemon.mcdevmanager.data.global.AppContext 9 | import com.orhanobut.logger.Logger 10 | import com.zj.mvi.core.SharedFlowEvents 11 | import com.zj.mvi.core.setEvent 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.flow.asSharedFlow 14 | import kotlinx.coroutines.flow.collect 15 | import kotlinx.coroutines.flow.flow 16 | import kotlinx.coroutines.launch 17 | 18 | class SplashViewModel : ViewModel() { 19 | private val _viewEvents = SharedFlowEvents() 20 | val viewEvents = _viewEvents.asSharedFlow() 21 | 22 | fun dispatch(action: SplashViewAction) { 23 | when (action) { 24 | is SplashViewAction.GetDatabase -> getDatabase() 25 | } 26 | } 27 | 28 | private fun getDatabase() { 29 | viewModelScope.launch(Dispatchers.IO) { 30 | flow { 31 | val userInfoList = GlobalDataBase.database.userDao().getAllUsers() 32 | userInfoList.let { 33 | if (userInfoList.isNotEmpty()) { 34 | for (user in userInfoList) { 35 | if (userInfoList.indexOf(user) == 0) 36 | AppContext.nowNickname = user.nickname 37 | AppContext.cookiesStore[user.nickname] = user.cookie 38 | } 39 | AppContext.accountList.addAll(userInfoList.map { it.nickname }) 40 | _viewEvents.setEvent(SplashViewEvent.RouteToPath(MAIN_PAGE)) 41 | } else { 42 | _viewEvents.setEvent(SplashViewEvent.RouteToPath(LOGIN_PAGE)) 43 | } 44 | } 45 | }.collect() 46 | } 47 | } 48 | } 49 | 50 | sealed class SplashViewAction { 51 | data object GetDatabase : SplashViewAction() 52 | } 53 | 54 | sealed class SplashViewEvent { 55 | data class RouteToPath(val path: String) : SplashViewEvent() 56 | } -------------------------------------------------------------------------------- /mvi-core/src/main/java/com/zj/mvi/core/MVIExt.kt: -------------------------------------------------------------------------------- 1 | package com.zj.mvi.core 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.distinctUntilChanged 7 | import androidx.lifecycle.map 8 | import kotlin.reflect.KProperty1 9 | 10 | fun LiveData.observeState( 11 | lifecycleOwner: LifecycleOwner, 12 | prop1: KProperty1, 13 | action: (A) -> Unit 14 | ) { 15 | this.map { 16 | StateTuple1(prop1.get(it)) 17 | }.distinctUntilChanged().observe(lifecycleOwner) { (a) -> 18 | action.invoke(a) 19 | } 20 | } 21 | 22 | fun LiveData.observeState( 23 | lifecycleOwner: LifecycleOwner, 24 | prop1: KProperty1, 25 | prop2: KProperty1, 26 | action: (A, B) -> Unit 27 | ) { 28 | this.map { 29 | StateTuple2(prop1.get(it), prop2.get(it)) 30 | }.distinctUntilChanged().observe(lifecycleOwner) { (a, b) -> 31 | action.invoke(a, b) 32 | } 33 | } 34 | 35 | fun LiveData.observeState( 36 | lifecycleOwner: LifecycleOwner, 37 | prop1: KProperty1, 38 | prop2: KProperty1, 39 | prop3: KProperty1, 40 | action: (A, B, C) -> Unit 41 | ) { 42 | this.map { 43 | StateTuple3(prop1.get(it), prop2.get(it), prop3.get(it)) 44 | }.distinctUntilChanged().observe(lifecycleOwner) { (a, b, c) -> 45 | action.invoke(a, b, c) 46 | } 47 | } 48 | 49 | internal data class StateTuple1(val a: A) 50 | internal data class StateTuple2(val a: A, val b: B) 51 | internal data class StateTuple3(val a: A, val b: B, val c: C) 52 | 53 | fun MutableLiveData.setState(reducer: T.() -> T) { 54 | this.value = this.value?.reducer() 55 | } 56 | 57 | fun SingleLiveEvents.setEvent(vararg values: T) { 58 | this.value = values.toList() 59 | } 60 | 61 | fun LiveEvents.setEvent(vararg values: T) { 62 | this.value = values.toList() 63 | } 64 | 65 | fun LiveData>.observeEvent(lifecycleOwner: LifecycleOwner, action: (T) -> Unit) { 66 | this.observe(lifecycleOwner) { 67 | it.forEach { event -> 68 | action.invoke(event) 69 | } 70 | } 71 | } 72 | 73 | inline fun withState(state: LiveData, block: (T) -> R): R? { 74 | return state.value?.let(block) 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/database/database/AppDataBase.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.database.database 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import androidx.room.migration.Migration 8 | import com.lemon.mcdevmanager.data.database.dao.InfoDao 9 | import com.lemon.mcdevmanager.data.database.dao.UserDao 10 | import com.lemon.mcdevmanager.data.database.entities.AnalyzeEntity 11 | import com.lemon.mcdevmanager.data.database.entities.OverviewEntity 12 | import com.lemon.mcdevmanager.data.database.entities.UserEntity 13 | 14 | @Database( 15 | entities = [UserEntity::class, OverviewEntity::class, AnalyzeEntity::class], 16 | version = 3 17 | ) 18 | abstract class AppDataBase : RoomDatabase() { 19 | companion object { 20 | @Volatile 21 | private var instance: AppDataBase? = null 22 | 23 | private const val DATABASE_NAME = "mcDevManager.db" 24 | 25 | fun getInstance(context: Context): AppDataBase { 26 | return instance ?: synchronized(this) { 27 | instance ?: Room.databaseBuilder( 28 | context, 29 | AppDataBase::class.java, 30 | DATABASE_NAME 31 | ).addMigrations(Migration(1, 2) { 32 | it.execSQL("CREATE TABLE IF NOT EXISTS `analyzeEntity` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nickname` TEXT NOT NULL, `filterType` INTEGER NOT NULL, `platform` TEXT NOT NULL, `startDate` TEXT NOT NULL, `endDate` TEXT NOT NULL, `filterResourceList` TEXT NOT NULL, `createTime` INTEGER NOT NULL)") 33 | }).addMigrations(Migration(2, 3) { 34 | it.execSQL("ALTER TABLE `overviewEntity` ADD COLUMN `lastMonthProfit` TEXT NOT NULL DEFAULT '0.00'") 35 | it.execSQL("ALTER TABLE `overviewEntity` ADD COLUMN `lastMonthTax` TEXT NOT NULL DEFAULT '0.00'") 36 | it.execSQL("ALTER TABLE `overviewEntity` ADD COLUMN `thisMonthProfit` TEXT NOT NULL DEFAULT '0.00'") 37 | it.execSQL("ALTER TABLE `overviewEntity` ADD COLUMN `thisMonthTax` TEXT NOT NULL DEFAULT '0.00'") 38 | }).build().also { instance = it } 39 | } 40 | } 41 | } 42 | 43 | abstract fun userDao(): UserDao 44 | 45 | abstract fun infoDao(): InfoDao 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/income/ApplyIncomeDetailBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.income 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApplyIncomeDetailBean( 8 | @SerialName("adjust_money") 9 | val adjustMoney: String = "0.00", 10 | @SerialName("available_detail") 11 | val availableDetail: List = emptyList(), 12 | @SerialName("available_income") 13 | val availableIncome: String = "", 14 | val bank: String = "", 15 | @SerialName("card_no") 16 | val cardNo: String = "", 17 | val city: String = "", 18 | @SerialName("currency_type") 19 | val currencyType: String = "", 20 | @SerialName("data_month") 21 | val dataMonth: String = "", 22 | @SerialName("developer_urs") 23 | val developerUrs: String = "", 24 | @SerialName("extra_info") 25 | val extraInfo: ExtraInfo = ExtraInfo(), 26 | val id: String = "", 27 | @SerialName("incentive_income") 28 | val incentiveIncome: String = "0.00", 29 | val income: String = "", 30 | val platform: String = "", 31 | @SerialName("play_plan_income") 32 | val playPlanIncome: String = "0.00", 33 | @SerialName("provider_name") 34 | val providerName: String = "", 35 | val province: String = "", 36 | @SerialName("real_name") 37 | val realName: String = "", 38 | val status: String = "", 39 | @SerialName("sub_bank") 40 | val subBank: String = "", 41 | val tax: String = "", 42 | @SerialName("tax_income") 43 | val taxIncome: String = "0.00", 44 | @SerialName("tech_service_fee") 45 | val techServiceFee: Double = 0.0, 46 | @SerialName("total_diamond") 47 | val totalDiamond: Int = 0, 48 | @SerialName("total_usage_price") 49 | val totalUsagePrice: Double = 0.0, 50 | @SerialName("type") 51 | private val _type: String = "" 52 | ){ 53 | val type: String 54 | get() = if (_type == "individual_withhold") "个人开发者代扣代缴" else if (_type == "individual_owned") "个人开发者自备税票" else if (_type == "company_owned") "公司开发者自备税票" else "---" 55 | } 56 | 57 | @Serializable 58 | data class ExtraInfo( 59 | @SerialName("adv_income") 60 | val advIncome: Double = 0.0 61 | ) 62 | 63 | @Serializable 64 | data class AvailableDetail( 65 | @SerialName("data_month") 66 | val dataMonth: String = "", 67 | val income: String = "" 68 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/InfoApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import com.lemon.mcdevmanager.data.common.JSONConverter 6 | import com.lemon.mcdevmanager.data.common.NETEASE_MC_DEV_LINK 7 | import com.lemon.mcdevmanager.data.netease.resource.ResourceResponseBean 8 | import com.lemon.mcdevmanager.data.netease.user.LevelInfoBean 9 | import com.lemon.mcdevmanager.data.netease.user.OverviewBean 10 | import com.lemon.mcdevmanager.data.netease.user.UserInfoBean 11 | import com.lemon.mcdevmanager.utils.ResponseData 12 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 13 | import okhttp3.OkHttpClient 14 | import retrofit2.Call 15 | import retrofit2.Retrofit 16 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 17 | import retrofit2.http.GET 18 | import retrofit2.http.Path 19 | import retrofit2.http.Query 20 | import java.util.concurrent.TimeUnit 21 | 22 | interface InfoApi { 23 | 24 | @GET("/users/me") 25 | fun getUserInfo(): Call> 26 | 27 | @GET("/data_analysis/overview") 28 | fun getOverview(): Call> 29 | 30 | @GET("/new_level") 31 | fun getLevelInfo(): Call> 32 | 33 | @GET("/items/categories/{platform}") 34 | suspend fun getResInfoList( 35 | @Path("platform") platform: String = "pe", 36 | @Query("start") start: Int = 0, 37 | @Query("span") span: Int = Int.MAX_VALUE 38 | ): ResponseData 39 | 40 | companion object { 41 | /** 42 | * 获取接口实例用于调用对接方法 43 | * @return ServerApi 44 | */ 45 | fun create(): InfoApi { 46 | val client = OkHttpClient.Builder() 47 | .connectTimeout(30, TimeUnit.SECONDS) 48 | .readTimeout(30, TimeUnit.SECONDS) 49 | .addInterceptor(AddCookiesInterceptor()) 50 | .addInterceptor(CommonInterceptor()) 51 | .build() 52 | return Retrofit.Builder() 53 | .baseUrl(NETEASE_MC_DEV_LINK) 54 | .addConverterFactory( 55 | JSONConverter.asConverterFactory( 56 | "application/json; charset=UTF8".toMediaTypeOrNull()!! 57 | ) 58 | ) 59 | .client(client) 60 | .build() 61 | .create(InfoApi::class.java) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/FeedbackApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import com.lemon.mcdevmanager.data.common.JSONConverter 6 | import com.lemon.mcdevmanager.data.common.NETEASE_MC_DEV_LINK 7 | import com.lemon.mcdevmanager.data.netease.feedback.FeedbackResponseBean 8 | import com.lemon.mcdevmanager.data.netease.feedback.ReplyBean 9 | import com.lemon.mcdevmanager.utils.NoNeedData 10 | import com.lemon.mcdevmanager.utils.ResponseData 11 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 12 | import okhttp3.OkHttpClient 13 | import okhttp3.RequestBody 14 | import retrofit2.Retrofit 15 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 16 | import retrofit2.http.Body 17 | import retrofit2.http.GET 18 | import retrofit2.http.PUT 19 | import retrofit2.http.Path 20 | import retrofit2.http.Query 21 | import java.util.concurrent.TimeUnit 22 | 23 | interface FeedbackApi { 24 | 25 | @GET("/items/feedback/pe/") 26 | suspend fun loadFeedback( 27 | @Query("start") from: Int, 28 | @Query("span") size: Int, 29 | @Query("sort") sort: String? = null, 30 | @Query("order") order: String? = null, 31 | @Query("type") status: String? = null, 32 | @Query("fuzzy_key") key: String? = null, 33 | @Query("reply_count") replyCount: Int? = null 34 | ): ResponseData 35 | 36 | @PUT("/items/feedback/pe/{id}/reply") 37 | suspend fun sendReply( 38 | @Path("id") feedbackId: String, 39 | @Body content: RequestBody 40 | ): ResponseData 41 | 42 | companion object { 43 | /** 44 | * 获取接口实例用于调用对接方法 45 | * @return ServerApi 46 | */ 47 | fun create(): FeedbackApi { 48 | val client = OkHttpClient.Builder() 49 | .connectTimeout(15, TimeUnit.SECONDS) 50 | .readTimeout(15, TimeUnit.SECONDS) 51 | .addInterceptor(AddCookiesInterceptor()) 52 | .addInterceptor(CommonInterceptor()) 53 | .build() 54 | return Retrofit.Builder() 55 | .baseUrl(NETEASE_MC_DEV_LINK) 56 | .addConverterFactory( 57 | JSONConverter.asConverterFactory( 58 | "application/json; charset=UTF8".toMediaTypeOrNull()!! 59 | ) 60 | ) 61 | .client(client) 62 | .build() 63 | .create(FeedbackApi::class.java) 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/LoginOutlineTextField.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material3.OutlinedTextField 8 | import androidx.compose.material3.OutlinedTextFieldDefaults 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.text.input.VisualTransformation 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | import com.lemon.mcdevmanager.ui.theme.AppTheme 16 | 17 | @Composable 18 | fun LoginOutlineTextField( 19 | modifier: Modifier = Modifier, 20 | value: String, 21 | onValueChange: (String) -> Unit, 22 | label: @Composable () -> Unit = {}, 23 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 24 | keyboardActions: KeyboardActions = KeyboardActions.Default, 25 | visualTransformation: VisualTransformation = VisualTransformation.None, 26 | singleLine: Boolean = true, 27 | trialingIcon: @Composable (() -> Unit)? = null 28 | ) { 29 | OutlinedTextField( 30 | value = value, 31 | onValueChange = onValueChange, 32 | label = label, 33 | modifier = Modifier 34 | .fillMaxWidth() 35 | .padding(horizontal = 20.dp) 36 | .then(modifier), 37 | colors = OutlinedTextFieldDefaults.colors( 38 | focusedBorderColor = AppTheme.colors.primaryColor, 39 | focusedTextColor = AppTheme.colors.textColor, 40 | focusedLabelColor = AppTheme.colors.primaryColor, 41 | focusedContainerColor = AppTheme.colors.card, 42 | unfocusedLabelColor = AppTheme.colors.secondaryColor, 43 | unfocusedBorderColor = AppTheme.colors.secondaryColor, 44 | unfocusedTextColor = AppTheme.colors.textColor, 45 | unfocusedContainerColor = AppTheme.colors.card 46 | ), 47 | keyboardOptions = keyboardOptions, 48 | singleLine = singleLine, 49 | keyboardActions = keyboardActions, 50 | visualTransformation = visualTransformation, 51 | trailingIcon = trialingIcon 52 | ) 53 | } 54 | 55 | @Composable 56 | @Preview(showBackground = true, showSystemUi = true) 57 | private fun LoginOutlineTextFieldPreview() { 58 | LoginOutlineTextField( 59 | value = "", 60 | onValueChange = {}, 61 | label = { 62 | Text("Username") 63 | } 64 | ) 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/utils/StaticUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.utils 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.os.Build 6 | import android.util.DisplayMetrics 7 | import android.view.WindowManager 8 | 9 | fun getNavigationBarHeight(context: Context): Int { 10 | val resources: Resources = context.resources 11 | val resourceId: Int = resources.getIdentifier("navigation_bar_height", "dimen", "android") 12 | return if (resourceId > 0) { 13 | pxToDp(context, resources.getDimensionPixelSize(resourceId).toFloat()) 14 | } else 0 15 | } 16 | 17 | fun getScreenWidth(context: Context): Int { 18 | val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 19 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 20 | return windowManager.currentWindowMetrics.bounds.width() 21 | } else { 22 | val displayMetrics = DisplayMetrics() 23 | windowManager.defaultDisplay.getMetrics(displayMetrics) 24 | return displayMetrics.widthPixels 25 | } 26 | } 27 | 28 | fun getScreenHeight(context: Context): Int { 29 | val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 31 | return windowManager.currentWindowMetrics.bounds.height() 32 | } else { 33 | val displayMetrics = DisplayMetrics() 34 | windowManager.defaultDisplay.getMetrics(displayMetrics) 35 | return displayMetrics.heightPixels 36 | } 37 | } 38 | 39 | fun pxToDp(context: Context, px: Float): Int { 40 | return Math.round(px / context.resources.displayMetrics.density) 41 | } 42 | 43 | fun dpToPx(context: Context, dp: Int): Int { 44 | return Math.round(dp * context.resources.displayMetrics.density) 45 | } 46 | 47 | fun getNoScaleTextSize(context: Context, textSize: Float): Float { 48 | val fontScale = getFontScale(context) 49 | if (fontScale > 1.0f) { 50 | return textSize / fontScale 51 | } 52 | return textSize 53 | } 54 | 55 | // 获取平均分布的元素 必须包含第一个和最后一个 56 | fun getAvgItems(list: List, count: Int): List { 57 | val result = mutableListOf() 58 | val size = list.size 59 | if (size <= count) { 60 | return list 61 | } 62 | val step = size / (count - 1) 63 | for (i in 0 until count) { 64 | val index = i * step 65 | if (index < size) { 66 | result.add(list[index]) 67 | } 68 | } 69 | if (list.last() != result.last()) { 70 | result.add(list.last()) 71 | } 72 | return result 73 | } 74 | 75 | fun getFontScale(context: Context): Float { 76 | return context.resources.configuration.fontScale 77 | } -------------------------------------------------------------------------------- /mvi-core/src/main/java/com/zj/mvi/core/LiveEvents.kt: -------------------------------------------------------------------------------- 1 | package com.zj.mvi.core 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.LifecycleOwner 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.Observer 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | /** 10 | * LiveEvents 11 | * 负责处理多维度一次性Event,支持多个监听者 12 | * 比如我们在请求开始时发出ShowLoading,网络请求成功后发出DismissLoading与Toast事件 13 | * 如果我们在请求开始后回到桌面,成功后再回到App,这样有一个事件就会被覆盖,因此将所有事件通过List存储 14 | */ 15 | class LiveEvents : MutableLiveData>() { 16 | 17 | private val observers = hashSetOf>() 18 | 19 | @MainThread 20 | override fun observe(owner: LifecycleOwner, observer: Observer>) { 21 | observers.find { it.observer === observer }?.let { _ -> // existing 22 | return 23 | } 24 | val wrapper = ObserverWrapper(observer) 25 | observers.add(wrapper) 26 | super.observe(owner, wrapper) 27 | } 28 | 29 | @MainThread 30 | override fun observeForever(observer: Observer>) { 31 | observers.find { it.observer === observer }?.let { _ -> // existing 32 | return 33 | } 34 | val wrapper = ObserverWrapper(observer) 35 | observers.add(wrapper) 36 | super.observeForever(wrapper) 37 | } 38 | 39 | @MainThread 40 | override fun removeObserver(observer: Observer>) { 41 | if (observer is ObserverWrapper<*> && observers.remove(observer)) { 42 | super.removeObserver(observer) 43 | return 44 | } 45 | val iterator = observers.iterator() 46 | while (iterator.hasNext()) { 47 | val wrapper = iterator.next() 48 | if (wrapper.observer == observer) { 49 | iterator.remove() 50 | super.removeObserver(wrapper) 51 | break 52 | } 53 | } 54 | } 55 | 56 | @MainThread 57 | override fun setValue(t: List?) { 58 | observers.forEach { it.newValue(t) } 59 | super.setValue(t) 60 | } 61 | 62 | private class ObserverWrapper(val observer: Observer>) : Observer> { 63 | 64 | private val pending = AtomicBoolean(false) 65 | private val eventList = mutableListOf>() 66 | override fun onChanged(value: List) { 67 | if (pending.compareAndSet(true, false)) { 68 | observer.onChanged(eventList.flatten()) 69 | eventList.clear() 70 | } 71 | } 72 | 73 | fun newValue(t: List?) { 74 | pending.set(true) 75 | t?.let { 76 | eventList.add(it) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 29 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 51 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/FABPositionWidget.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.offset 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.layout.onGloballyPositioned 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.tooling.preview.Preview 24 | import androidx.compose.ui.unit.IntOffset 25 | import androidx.compose.ui.unit.dp 26 | import com.lemon.mcdevmanager.utils.pxToDp 27 | import kotlin.math.roundToInt 28 | 29 | @Composable 30 | fun FABPositionWidget( 31 | content: @Composable (ColumnScope.() -> Unit) = { } 32 | ) { 33 | var fullHeight by remember { mutableIntStateOf(0) } 34 | var fabHeight by remember { mutableIntStateOf(0) } 35 | val context = LocalContext.current 36 | Box( 37 | modifier = Modifier 38 | .fillMaxSize() 39 | .onGloballyPositioned { fullHeight = it.size.height } 40 | ) { 41 | Box( 42 | modifier = Modifier 43 | .align(Alignment.CenterEnd) 44 | .padding(end = 32.dp) 45 | .width(60.dp) 46 | ) { 47 | Column( 48 | modifier = Modifier 49 | .fillMaxWidth() 50 | .onGloballyPositioned { fabHeight = it.size.height } 51 | .offset { 52 | IntOffset( 53 | x = 0, 54 | y = pxToDp(context, fullHeight.toFloat()) - pxToDp( 55 | context, fabHeight.toFloat()) 56 | ) 57 | } 58 | ) { content() } 59 | } 60 | } 61 | } 62 | 63 | @Composable 64 | @Preview(showBackground = true, showSystemUi = true) 65 | private fun FABPositionWidgetPreview() { 66 | FABPositionWidget { 67 | Box( 68 | modifier = Modifier 69 | .fillMaxWidth() 70 | .height(300.dp) 71 | .background(Color.Black) 72 | .padding(16.dp) 73 | ) { 74 | // Your content here 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/IncomeApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import com.lemon.mcdevmanager.data.common.JSONConverter 6 | import com.lemon.mcdevmanager.data.common.NETEASE_MC_DEV_LINK 7 | import com.lemon.mcdevmanager.data.netease.income.ApplyIncomeDetailBean 8 | import com.lemon.mcdevmanager.data.netease.income.IncentiveBean 9 | import com.lemon.mcdevmanager.data.netease.income.IncentiveListBean 10 | import com.lemon.mcdevmanager.data.netease.income.IncomeDetailBean 11 | import com.lemon.mcdevmanager.utils.NoNeedData 12 | import com.lemon.mcdevmanager.utils.ResponseData 13 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 14 | import okhttp3.OkHttpClient 15 | import okhttp3.RequestBody 16 | import retrofit2.Retrofit 17 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 18 | import retrofit2.http.Body 19 | import retrofit2.http.GET 20 | import retrofit2.http.PUT 21 | import retrofit2.http.Path 22 | import retrofit2.http.Query 23 | import java.util.concurrent.TimeUnit 24 | 25 | interface IncomeApi { 26 | 27 | // 结算收益 28 | @PUT("/incomes/apply") 29 | suspend fun applyIncome( 30 | @Body request: RequestBody 31 | ): ResponseData 32 | 33 | // 获取结算信息 34 | @GET("/incomes") 35 | suspend fun getIncome( 36 | @Query("platform") platform: String = "pe", 37 | @Query("start") start: Int = 0, 38 | @Query("span") span: Int = Int.MAX_VALUE 39 | ): ResponseData 40 | 41 | // 获取结算详情 42 | @GET("/incomes/{id}") 43 | suspend fun getApplyDetail( 44 | @Path("id") id: String 45 | ): ResponseData 46 | 47 | // 获取激励金 48 | @GET("/incentive_fund/detail") 49 | suspend fun getIncentiveFund( 50 | @Query("platform") platform: String = "pe", 51 | @Query("start") start: Int = 0, 52 | @Query("span") span: Int = Int.MAX_VALUE 53 | ): ResponseData 54 | 55 | companion object { 56 | /** 57 | * 获取接口实例用于调用对接方法 58 | * @return ServerApi 59 | */ 60 | fun create(): IncomeApi { 61 | val client = OkHttpClient.Builder() 62 | .connectTimeout(15, TimeUnit.SECONDS) 63 | .readTimeout(15, TimeUnit.SECONDS) 64 | .addInterceptor(AddCookiesInterceptor()) 65 | .addInterceptor(CommonInterceptor()) 66 | .build() 67 | return Retrofit.Builder() 68 | .baseUrl(NETEASE_MC_DEV_LINK) 69 | .addConverterFactory( 70 | JSONConverter.asConverterFactory( 71 | "application/json; charset=UTF8".toMediaTypeOrNull()!! 72 | ) 73 | ) 74 | .client(client) 75 | .build() 76 | .create(IncomeApi::class.java) 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.utils 2 | 3 | import android.content.ContentValues 4 | import android.content.Context 5 | import android.os.Build 6 | import android.os.Environment 7 | import android.provider.MediaStore 8 | import com.orhanobut.logger.Logger 9 | import java.io.File 10 | import java.io.FileInputStream 11 | import java.io.FileOutputStream 12 | import java.io.IOException 13 | 14 | fun copyFileToDownloadFolder( 15 | context: Context, 16 | sourcePath: String, 17 | fileName: String, 18 | targetPath: String, 19 | onSuccess: () -> Unit, 20 | onFail: () -> Unit 21 | ) { 22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 23 | Logger.d("MCDevManager" + File.pathSeparator + "Log" + File.pathSeparator + fileName) 24 | val resolver = context.contentResolver 25 | val contentValues = ContentValues().apply { 26 | put( 27 | MediaStore.Downloads.DISPLAY_NAME, 28 | "MCDevManager" + File.pathSeparator + "Log" + File.pathSeparator + fileName 29 | ) 30 | put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream") 31 | put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) 32 | } 33 | val uri = resolver.insert( 34 | MediaStore.Downloads.EXTERNAL_CONTENT_URI, 35 | contentValues 36 | ) 37 | uri?.let { 38 | try { 39 | resolver.openOutputStream(it)?.use { outputStream -> 40 | val file = File(sourcePath + File.separator + fileName) 41 | FileInputStream(file).use { inputStream -> 42 | inputStream.copyTo(outputStream) 43 | } 44 | } 45 | onSuccess() 46 | } catch (e: IOException) { 47 | Logger.e(e, "文件复制至下载目录失败: ${e.message}") 48 | onFail() 49 | } 50 | } ?: run { 51 | onFail() 52 | Logger.e("文件复制至下载目录失败: uri is null") 53 | } 54 | } else { 55 | val sourceFile = File(sourcePath) 56 | val downloadsDir = Environment.getExternalStoragePublicDirectory(targetPath) 57 | if (!downloadsDir.exists()) { 58 | downloadsDir.mkdirs() 59 | } 60 | val destinationFile = File(downloadsDir, fileName) 61 | 62 | try { 63 | FileInputStream(sourceFile).use { input -> 64 | FileOutputStream(destinationFile).use { output -> 65 | input.copyTo(output) 66 | } 67 | } 68 | onSuccess() 69 | } catch (e: IOException) { 70 | Logger.e(e, "日志文件导出失败: ${e.message}") 71 | onFail() 72 | } 73 | } 74 | } 75 | 76 | fun getFileSizeFormat(size: Long): String { 77 | val kb = size / 1024 78 | return if (kb < 1024) { 79 | "$kb KB" 80 | } else if (kb < 1024 * 1024) { 81 | val mb = kb / 1024 82 | "$mb MB" 83 | } else { 84 | val gb = kb / 1024 85 | "$gb GB" 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.SystemBarStyle 8 | import androidx.activity.compose.setContent 9 | import androidx.activity.enableEdgeToEdge 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.WindowInsets 13 | import androidx.compose.foundation.layout.WindowInsetsSides 14 | import androidx.compose.foundation.layout.asPaddingValues 15 | import androidx.compose.foundation.layout.fillMaxSize 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.statusBars 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.toArgb 21 | import androidx.core.view.WindowCompat 22 | import com.lemon.mcdevmanager.data.global.AppContext 23 | import com.lemon.mcdevmanager.ui.base.BaseScaffold 24 | import com.lemon.mcdevmanager.ui.theme.AppTheme 25 | import com.lemon.mcdevmanager.ui.theme.MCDevManagerTheme 26 | import com.lemon.mcdevmanager.ui.theme.Purple200 27 | import com.lemon.mcdevmanager.ui.theme.Purple40 28 | import com.orhanobut.logger.AndroidLogAdapter 29 | import com.orhanobut.logger.DiskLogAdapter 30 | import com.orhanobut.logger.FormatStrategy 31 | import com.orhanobut.logger.Logger 32 | import com.orhanobut.logger.PrettyFormatStrategy 33 | import java.io.File 34 | import java.text.SimpleDateFormat 35 | import java.util.Locale 36 | 37 | 38 | class MainActivity : ComponentActivity() { 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | // 初始化日志 41 | val formatStrategy: FormatStrategy = 42 | PrettyFormatStrategy.newBuilder().showThreadInfo(true).methodCount(4).tag("MCDevLogger") 43 | .build() 44 | Logger.addLogAdapter(AndroidLogAdapter(formatStrategy)) 45 | val fileName = SimpleDateFormat( 46 | "yyyy_MM_dd-HH:mm:ss", 47 | Locale.CHINA 48 | ).format(System.currentTimeMillis()) + ".log" 49 | val logDirPath = 50 | this.getExternalFilesDir("logger" + File.separatorChar + "mcDevMng")?.absolutePath ?: "" 51 | AppContext.logDirPath = logDirPath 52 | Logger.addLogAdapter(DiskLogAdapter(fileName, logDirPath)) 53 | 54 | // 允许在状态栏渲染内容 55 | WindowCompat.setDecorFitsSystemWindows(window, false) 56 | 57 | enableEdgeToEdge( 58 | // 透明状态栏 59 | statusBarStyle = SystemBarStyle.auto( 60 | android.graphics.Color.TRANSPARENT, 61 | android.graphics.Color.TRANSPARENT, 62 | ), 63 | // 透明导航栏 64 | navigationBarStyle = SystemBarStyle.auto( 65 | android.graphics.Color.TRANSPARENT, 66 | android.graphics.Color.TRANSPARENT, 67 | ) 68 | ) 69 | 70 | super.onCreate(savedInstanceState) 71 | setContent { 72 | MCDevManagerTheme { 73 | BaseScaffold() 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /mvi-core/src/main/java/com/zj/mvi/core/MVIFlowExt.kt: -------------------------------------------------------------------------------- 1 | package com.zj.mvi.core 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleEventObserver 6 | import androidx.lifecycle.LifecycleOwner 7 | import androidx.lifecycle.lifecycleScope 8 | import androidx.lifecycle.repeatOnLifecycle 9 | import kotlinx.coroutines.Job 10 | import kotlinx.coroutines.flow.* 11 | import kotlinx.coroutines.launch 12 | import kotlin.reflect.KProperty1 13 | 14 | /** 15 | * flow部分 16 | */ 17 | fun StateFlow.observeState( 18 | lifecycleOwner: LifecycleOwner, 19 | prop1: KProperty1, 20 | action: (A) -> Unit 21 | ) { 22 | lifecycleOwner.lifecycleScope.launch { 23 | lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 24 | this@observeState.map { 25 | StateTuple1(prop1.get(it)) 26 | }.distinctUntilChanged().collect { (a) -> 27 | action.invoke(a) 28 | } 29 | } 30 | } 31 | } 32 | 33 | fun StateFlow.observeState( 34 | lifecycleOwner: LifecycleOwner, 35 | prop1: KProperty1, 36 | prop2: KProperty1, 37 | action: (A, B) -> Unit 38 | ) { 39 | lifecycleOwner.lifecycleScope.launch { 40 | lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 41 | this@observeState.map { 42 | StateTuple2(prop1.get(it), prop2.get(it)) 43 | }.distinctUntilChanged().collect { (a, b) -> 44 | action.invoke(a, b) 45 | } 46 | } 47 | } 48 | } 49 | 50 | fun StateFlow.observeState( 51 | lifecycleOwner: LifecycleOwner, 52 | prop1: KProperty1, 53 | prop2: KProperty1, 54 | prop3: KProperty1, 55 | action: (A, B, C) -> Unit 56 | ) { 57 | lifecycleOwner.lifecycleScope.launch { 58 | lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 59 | this@observeState.map { 60 | StateTuple3(prop1.get(it), prop2.get(it), prop3.get(it)) 61 | }.distinctUntilChanged().collect { (a, b, c) -> 62 | action.invoke(a, b, c) 63 | } 64 | } 65 | } 66 | } 67 | 68 | fun MutableStateFlow.setState(reducer: T.() -> T) { 69 | this.value = this.value.reducer() 70 | } 71 | 72 | inline fun withState(state: StateFlow, block: (T) -> R): R { 73 | return state.value.let(block) 74 | } 75 | 76 | suspend fun SharedFlowEvents.setEvent(vararg values: T) { 77 | val eventList = values.toList() 78 | this.emit(eventList) 79 | } 80 | 81 | fun SharedFlow>.observeEvent(lifecycleOwner: LifecycleOwner, action: (T) -> Unit): Job { 82 | return lifecycleOwner.lifecycleScope.launch { 83 | lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.STARTED) { 84 | this@observeEvent.collect { 85 | it.forEach { event -> 86 | action.invoke(event) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | typealias SharedFlowEvents = MutableSharedFlow> 94 | 95 | @Suppress("FunctionName") 96 | fun SharedFlowEvents(): SharedFlowEvents { 97 | return MutableSharedFlow() 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/NavigationItem.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxHeight 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.shape.CircleShape 13 | import androidx.compose.material.ripple 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.graphics.ColorFilter 22 | import androidx.compose.ui.layout.ContentScale 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import com.lemon.mcdevmanager.R 28 | import com.lemon.mcdevmanager.ui.theme.AppTheme 29 | 30 | @Composable 31 | fun NavigationItem( 32 | title: String, 33 | icon: Int, 34 | isSelected: Boolean = false, 35 | colorList: List = listOf(AppTheme.colors.primaryColor, AppTheme.colors.secondaryColor), 36 | onClick: () -> Unit 37 | ) { 38 | val isPureEnglish = title.matches(Regex("^[a-zA-Z]*$")) 39 | Box( 40 | contentAlignment = Alignment.Center, 41 | modifier = Modifier.fillMaxHeight() 42 | ) { 43 | Column( 44 | horizontalAlignment = Alignment.CenterHorizontally, 45 | modifier = Modifier.padding(4.dp) 46 | ) { 47 | Image( 48 | painter = painterResource(id = icon), 49 | contentDescription = title, 50 | modifier = Modifier.size(24.dp), 51 | colorFilter = ColorFilter.tint( 52 | if (isSelected) colorList[0] else colorList[1] 53 | ), 54 | contentScale = ContentScale.Fit 55 | ) 56 | Text( 57 | text = title, 58 | fontSize = 12.sp, 59 | letterSpacing = if (isPureEnglish) 0.5.sp else 5.sp, 60 | color = if (isSelected) colorList[0] else colorList[1] 61 | ) 62 | } 63 | Box( 64 | modifier = Modifier 65 | .size(40.dp) 66 | .clip(CircleShape) 67 | .clickable( 68 | interactionSource = remember { MutableInteractionSource() }, 69 | indication = ripple(), 70 | onClick = onClick 71 | ) 72 | ) 73 | } 74 | 75 | } 76 | 77 | @Composable 78 | @Preview 79 | private fun NavigationItemPreview() { 80 | NavigationItem( 81 | title = "分析", 82 | icon = R.drawable.ic_bar_chart, 83 | isSelected = true 84 | ) {} 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/viewModel/IncentiveViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.viewModel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.lemon.mcdevmanager.data.netease.income.IncentiveBean 6 | import com.lemon.mcdevmanager.data.repository.IncomeRepository 7 | import com.lemon.mcdevmanager.ui.widget.SNACK_ERROR 8 | import com.lemon.mcdevmanager.ui.widget.SNACK_INFO 9 | import com.lemon.mcdevmanager.utils.NetworkState 10 | import com.zj.mvi.core.SharedFlowEvents 11 | import com.zj.mvi.core.setEvent 12 | import com.zj.mvi.core.setState 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.flow.asSharedFlow 16 | import kotlinx.coroutines.flow.asStateFlow 17 | import kotlinx.coroutines.flow.catch 18 | import kotlinx.coroutines.flow.collect 19 | import kotlinx.coroutines.flow.flow 20 | import kotlinx.coroutines.flow.flowOn 21 | import kotlinx.coroutines.flow.onCompletion 22 | import kotlinx.coroutines.flow.onStart 23 | import kotlinx.coroutines.launch 24 | 25 | class IncentiveViewModel : ViewModel() { 26 | private val repository = IncomeRepository.getInstance() 27 | private val _viewStates = MutableStateFlow(IncentiveViewStates()) 28 | val viewStates = _viewStates.asStateFlow() 29 | private val _viewEvents = SharedFlowEvents() 30 | val viewEvents = _viewEvents.asSharedFlow() 31 | 32 | fun dispatch(action: IncentiveViewActions) { 33 | when (action) { 34 | IncentiveViewActions.LoadData -> { 35 | loadData() 36 | } 37 | } 38 | } 39 | 40 | private fun loadData() { 41 | viewModelScope.launch { 42 | flow { 43 | loadDataLogic() 44 | }.onStart { 45 | _viewStates.setState { copy(isLoading = true) } 46 | }.onCompletion { 47 | _viewStates.setState { copy(isLoading = false) } 48 | }.catch { e -> 49 | _viewEvents.setEvent(IncentiveViewEvents.ShowToast(e.message ?: "", SNACK_ERROR)) 50 | }.flowOn(Dispatchers.IO).collect() 51 | } 52 | } 53 | 54 | private suspend fun loadDataLogic() { 55 | when (val result = repository.getIncentiveFund()) { 56 | is NetworkState.Success -> { 57 | result.data?.let { 58 | val list = it.incentiveDetails 59 | _viewStates.setState { 60 | copy(incentiveList = list.sortedBy { it.updateTime }.reversed()) 61 | } 62 | } ?: _viewEvents.setEvent(IncentiveViewEvents.ShowToast("数据为空", SNACK_INFO)) 63 | } 64 | 65 | is NetworkState.Error -> { 66 | _viewEvents.setEvent( 67 | IncentiveViewEvents.ShowToast("获取数据失败: ${result.msg}", SNACK_ERROR) 68 | ) 69 | } 70 | } 71 | } 72 | } 73 | 74 | data class IncentiveViewStates( 75 | val isLoading: Boolean = false, 76 | val incentiveList: List = emptyList(), 77 | ) 78 | 79 | sealed class IncentiveViewActions { 80 | data object LoadData : IncentiveViewActions() 81 | } 82 | 83 | sealed class IncentiveViewEvents { 84 | data class ShowToast(val message: String, val flag: String) : IncentiveViewEvents() 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/repository/FeedbackRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.repository 2 | 3 | import com.lemon.mcdevmanager.api.FeedbackApi 4 | import com.lemon.mcdevmanager.data.common.CookiesStore 5 | import com.lemon.mcdevmanager.data.common.NETEASE_USER_COOKIE 6 | import com.lemon.mcdevmanager.data.global.AppContext 7 | import com.lemon.mcdevmanager.data.netease.feedback.FeedbackResponseBean 8 | import com.lemon.mcdevmanager.data.netease.feedback.ReplyBean 9 | import com.lemon.mcdevmanager.utils.CookiesExpiredException 10 | import com.lemon.mcdevmanager.utils.NetworkState 11 | import com.lemon.mcdevmanager.utils.NoNeedData 12 | import com.lemon.mcdevmanager.utils.ResponseData 13 | import com.lemon.mcdevmanager.utils.UnifiedExceptionHandler 14 | import com.lemon.mcdevmanager.utils.dataJsonToString 15 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 16 | import okhttp3.RequestBody 17 | import okhttp3.RequestBody.Companion.toRequestBody 18 | 19 | class FeedbackRepository { 20 | companion object { 21 | @Volatile 22 | private var instance: FeedbackRepository? = null 23 | fun getInstance() = instance ?: synchronized(this) { 24 | instance ?: FeedbackRepository().also { instance = it } 25 | } 26 | } 27 | 28 | suspend fun loadFeedback( 29 | page: Int, 30 | keyword: String = "", 31 | order: String = "DESC", 32 | types: List = emptyList(), 33 | replyCount: Int = -1 34 | ): NetworkState { 35 | val start = (page - 1) * 20 36 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 37 | val keywordStr = keyword.ifEmpty { null } 38 | val typeStr = if (types.isNotEmpty()) types.joinToString("__") else null 39 | val realReplyCount = if (replyCount != -1) replyCount else null 40 | val orderStr = if (order == "DESC") null else "ASC" 41 | val sortStr = if (orderStr != null) "create_time" else null 42 | cookie?.let { 43 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 44 | return UnifiedExceptionHandler.handleSuspend { 45 | FeedbackApi.create().loadFeedback( 46 | from = start, 47 | size = 20, 48 | sort = sortStr, 49 | order = orderStr, 50 | status = typeStr, 51 | key = keywordStr, 52 | replyCount = realReplyCount 53 | ) 54 | } 55 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 56 | 57 | } 58 | 59 | suspend fun sendReply( 60 | feedbackId: String, 61 | content: String 62 | ): NetworkState { 63 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 64 | cookie?.let { 65 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 66 | val realContent = dataJsonToString(ReplyBean(content)) 67 | val requestBody = 68 | realContent.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) 69 | return UnifiedExceptionHandler.handleSuspend { 70 | FeedbackApi.create().sendReply(feedbackId, requestBody) 71 | } 72 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/repository/LoginRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.repository 2 | 3 | import com.lemon.mcdevmanager.api.LoginApi 4 | import com.lemon.mcdevmanager.data.common.RSAKey 5 | import com.lemon.mcdevmanager.data.common.SM4Key 6 | import com.lemon.mcdevmanager.data.netease.login.EncParams 7 | import com.lemon.mcdevmanager.data.netease.login.GetCapIdRequestBean 8 | import com.lemon.mcdevmanager.data.netease.login.GetPowerRequestBean 9 | import com.lemon.mcdevmanager.data.netease.login.LoginRequestBean 10 | import com.lemon.mcdevmanager.data.netease.login.PVResultStrBean 11 | import com.lemon.mcdevmanager.data.netease.login.TicketRequestBean 12 | import com.lemon.mcdevmanager.utils.NetworkState 13 | import com.lemon.mcdevmanager.utils.UnifiedExceptionHandler 14 | import com.lemon.mcdevmanager.utils.dataJsonToString 15 | import com.lemon.mcdevmanager.utils.rsaEncrypt 16 | import com.lemon.mcdevmanager.utils.sm4Encrypt 17 | 18 | class LoginRepository { 19 | 20 | companion object { 21 | @Volatile 22 | private var instance: LoginRepository? = null 23 | fun getInstance() = instance ?: synchronized(this) { 24 | instance ?: LoginRepository().also { instance = it } 25 | } 26 | } 27 | 28 | suspend fun init(topUrl: String): NetworkState { 29 | return UnifiedExceptionHandler.handleSuspendWithNeteaseData { 30 | val initRequest = GetCapIdRequestBean(topURL = topUrl) 31 | val encode = sm4Encrypt(dataJsonToString(initRequest), SM4Key) 32 | val encParams = EncParams(encode) 33 | LoginApi.create().init(encParams) 34 | } 35 | } 36 | 37 | suspend fun getPower(username: String, topUrl: String): NetworkState { 38 | return UnifiedExceptionHandler.handleSuspendWithNeteaseData { 39 | val powerRequest = GetPowerRequestBean(un = username, topURL = topUrl) 40 | val encode = sm4Encrypt(dataJsonToString(powerRequest), SM4Key) 41 | val encParams = EncParams(encode) 42 | LoginApi.create().getPower(encParams) 43 | } 44 | } 45 | 46 | suspend fun getTicket(username: String, topUrl: String): NetworkState { 47 | return UnifiedExceptionHandler.handleSuspendWithNeteaseData { 48 | val tkRequest = TicketRequestBean(username, topURL = topUrl) 49 | val encParams = EncParams(sm4Encrypt(dataJsonToString(tkRequest), SM4Key)) 50 | LoginApi.create() 51 | .getTicket(encParams) 52 | } 53 | } 54 | 55 | suspend fun loginWithTicket( 56 | username: String, 57 | password: String, 58 | ticket: String, 59 | pvResultBean: PVResultStrBean 60 | ): NetworkState { 61 | return UnifiedExceptionHandler.handleSuspendWithNeteaseData { 62 | val encodePw = rsaEncrypt(password, RSAKey) 63 | val loginRequest = LoginRequestBean( 64 | un = username, 65 | pw = encodePw, 66 | tk = ticket, 67 | topURL = "https://mcdev.webapp.163.com/#/login", 68 | pvParam = pvResultBean 69 | ) 70 | val encode = sm4Encrypt(dataJsonToString(loginRequest), SM4Key) 71 | val encParams = EncParams(encode) 72 | LoginApi.create().safeLogin(encParams) 73 | } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/TipsCard.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.material3.Card 13 | import androidx.compose.material3.CardDefaults 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.layout.ContentScale 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.text.style.TextOverflow 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import com.lemon.mcdevmanager.R 25 | import com.lemon.mcdevmanager.ui.theme.AppTheme 26 | 27 | @Composable 28 | fun TipsCard( 29 | modifier: Modifier = Modifier, 30 | @DrawableRes headerIcon: Int? = null, 31 | content: String, 32 | dismissText: String, 33 | onDismiss: () -> Unit 34 | ) { 35 | Card( 36 | modifier = Modifier 37 | .fillMaxWidth() 38 | .padding(8.dp) 39 | .then(modifier), 40 | colors = CardDefaults.cardColors( 41 | containerColor = AppTheme.colors.card 42 | ) 43 | ) { 44 | Row( 45 | Modifier 46 | .fillMaxWidth() 47 | .padding(16.dp) 48 | ) { 49 | if (headerIcon != null) { 50 | Image( 51 | painter = painterResource(id = headerIcon), 52 | contentDescription = "header icon", 53 | modifier = Modifier 54 | .size(24.dp) 55 | .align(Alignment.CenterVertically), 56 | contentScale = ContentScale.Fit 57 | ) 58 | Spacer(modifier = Modifier.width(16.dp)) 59 | } 60 | Text( 61 | text = content, 62 | color = AppTheme.colors.textColor, 63 | fontSize = 16.sp, 64 | modifier = Modifier 65 | .align(Alignment.CenterVertically) 66 | .weight(1f) 67 | .padding(end = 16.dp), 68 | overflow = TextOverflow.Ellipsis, 69 | maxLines = 1 70 | ) 71 | Text( 72 | text = dismissText, 73 | color = AppTheme.colors.info, 74 | fontSize = 16.sp, 75 | modifier = Modifier 76 | .align(Alignment.CenterVertically) 77 | .padding(start = 16.dp) 78 | .clickable { onDismiss() } 79 | ) 80 | } 81 | } 82 | } 83 | 84 | @Composable 85 | @Preview(showBackground = true, showSystemUi = true) 86 | private fun TipsCardPreview() { 87 | TipsCard( 88 | headerIcon = R.drawable.ic_notice, 89 | content = "这是一个提示卡片", 90 | dismissText = "知道了", 91 | onDismiss = {} 92 | ) 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/repository/IncomeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.repository 2 | 3 | import com.lemon.mcdevmanager.api.IncomeApi 4 | import com.lemon.mcdevmanager.data.common.CookiesStore 5 | import com.lemon.mcdevmanager.data.common.NETEASE_USER_COOKIE 6 | import com.lemon.mcdevmanager.data.global.AppContext 7 | import com.lemon.mcdevmanager.data.netease.income.IncentiveListBean 8 | import com.lemon.mcdevmanager.data.netease.income.ApplyIncomeBean 9 | import com.lemon.mcdevmanager.data.netease.income.ApplyIncomeDetailBean 10 | import com.lemon.mcdevmanager.data.netease.income.IncomeDetailBean 11 | import com.lemon.mcdevmanager.utils.CookiesExpiredException 12 | import com.lemon.mcdevmanager.utils.NetworkState 13 | import com.lemon.mcdevmanager.utils.NoNeedData 14 | import com.lemon.mcdevmanager.utils.UnifiedExceptionHandler 15 | import com.lemon.mcdevmanager.utils.dataJsonToString 16 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 17 | import okhttp3.RequestBody.Companion.toRequestBody 18 | 19 | class IncomeRepository { 20 | companion object { 21 | @Volatile 22 | private var instance: IncomeRepository? = null 23 | fun getInstance() = instance ?: synchronized(this) { 24 | instance ?: IncomeRepository().also { instance = it } 25 | } 26 | } 27 | 28 | 29 | suspend fun applyIncome(incomeIds: List): NetworkState { 30 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 31 | cookie?.let { 32 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 33 | 34 | val incomeData = dataJsonToString(ApplyIncomeBean(incomeIds)) 35 | val incomeBody = incomeData.toRequestBody("application/json".toMediaTypeOrNull()) 36 | return UnifiedExceptionHandler.handleSuspend { 37 | IncomeApi.create().applyIncome(incomeBody) 38 | } 39 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 40 | } 41 | 42 | suspend fun getApplyIncomeDetail(id: String): NetworkState { 43 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 44 | cookie?.let { 45 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 46 | return UnifiedExceptionHandler.handleSuspend { 47 | IncomeApi.create().getApplyDetail(id) 48 | } 49 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 50 | } 51 | 52 | suspend fun getIncomeDetail(platform: String = "pe"): NetworkState { 53 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 54 | cookie?.let { 55 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 56 | return UnifiedExceptionHandler.handleSuspend { 57 | IncomeApi.create().getIncome(platform) 58 | } 59 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 60 | } 61 | 62 | suspend fun getIncentiveFund(): NetworkState { 63 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 64 | cookie?.let { 65 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 66 | return UnifiedExceptionHandler.handleSuspend { 67 | IncomeApi.create().getIncentiveFund() 68 | } 69 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/SelectableItem.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.RowScope 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material.ripple 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.ColorFilter 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import com.lemon.mcdevmanager.R 28 | import com.lemon.mcdevmanager.ui.theme.AppTheme 29 | import com.lt.compose_views.other.HorizontalSpace 30 | 31 | @Composable 32 | fun SelectableItem( 33 | containItem: @Composable RowScope.() -> Unit = {}, 34 | isSelected: Boolean = false, 35 | onClick: (Boolean) -> Unit = {} 36 | ) { 37 | Row(modifier = Modifier 38 | .fillMaxWidth() 39 | .padding(horizontal = 8.dp, vertical = 4.dp) 40 | .clickable(indication = ripple(), 41 | interactionSource = remember { MutableInteractionSource() }) { onClick(isSelected) } 42 | ) { 43 | containItem() 44 | Spacer(modifier = Modifier.weight(1f)) 45 | if (isSelected) { 46 | Image( 47 | painter = painterResource(id = R.drawable.ic_correct), 48 | contentDescription = "selected", 49 | modifier = Modifier 50 | .size(30.dp) 51 | .padding(4.dp) 52 | .align(Alignment.CenterVertically), 53 | colorFilter = ColorFilter.tint(AppTheme.colors.primaryColor) 54 | ) 55 | } else { 56 | Box( 57 | modifier = Modifier 58 | .size(30.dp) 59 | .padding(4.dp) 60 | .align(Alignment.CenterVertically) 61 | ) 62 | } 63 | } 64 | } 65 | 66 | @Composable 67 | @Preview(showBackground = true, showSystemUi = true) 68 | private fun SelectableItemPreview() { 69 | var isSelected by remember { mutableStateOf(false) } 70 | SelectableItem( 71 | containItem = { 72 | Text( 73 | text = "Item", 74 | fontSize = 16.sp, 75 | color = AppTheme.colors.textColor, 76 | modifier = Modifier.align(Alignment.CenterVertically) 77 | ) 78 | HorizontalSpace(dp = 12.dp) 79 | Text( 80 | text = "id12312312312", 81 | color = AppTheme.colors.hintColor, 82 | fontSize = 14.sp, 83 | modifier = Modifier.align(Alignment.CenterVertically) 84 | ) 85 | }, isSelected = isSelected 86 | ) { 87 | isSelected = !it 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/FunctionCard.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.interaction.MutableInteractionSource 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.aspectRatio 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.offset 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material.ripple 15 | import androidx.compose.material3.Card 16 | import androidx.compose.material3.CardDefaults 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.alpha 23 | import androidx.compose.ui.draw.clip 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.layout.ContentScale 26 | import androidx.compose.ui.res.painterResource 27 | import androidx.compose.ui.text.font.Font 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.text.font.toFontFamily 30 | import androidx.compose.ui.tooling.preview.Preview 31 | import androidx.compose.ui.unit.dp 32 | import androidx.compose.ui.unit.sp 33 | import com.lemon.mcdevmanager.R 34 | import com.lemon.mcdevmanager.ui.theme.AppTheme 35 | 36 | @Composable 37 | fun FunctionCard( 38 | color: Color = AppTheme.colors.card, 39 | textColor: Color = AppTheme.colors.textColor, 40 | @DrawableRes icon: Int, 41 | title: String, 42 | onClick: () -> Unit = {} 43 | ) { 44 | Card( 45 | modifier = Modifier 46 | .fillMaxWidth() 47 | .padding(8.dp) 48 | .clip(RoundedCornerShape(8.dp)) 49 | .clickable( 50 | interactionSource = remember { MutableInteractionSource() }, 51 | indication = ripple(), 52 | onClick = onClick 53 | ), 54 | colors = CardDefaults.cardColors( 55 | containerColor = color 56 | ), 57 | shape = RoundedCornerShape(8.dp) 58 | ) { 59 | Box( 60 | modifier = Modifier 61 | .fillMaxWidth() 62 | .height(120.dp) 63 | ) { 64 | Image( 65 | painter = painterResource(id = icon), 66 | contentDescription = "analyze", 67 | modifier = Modifier 68 | .fillMaxWidth(0.45f) 69 | .aspectRatio(1f) 70 | .offset(x = (-20).dp, y = (-40).dp) 71 | .alpha(0.35f), 72 | contentScale = ContentScale.Fit 73 | ) 74 | Text( 75 | text = title, 76 | color = textColor, 77 | modifier = Modifier 78 | .padding(16.dp) 79 | .align(Alignment.BottomEnd), 80 | fontSize = 24.sp, 81 | fontFamily = Font(R.font.minecraft_ae).toFontFamily(), 82 | letterSpacing = 5.sp, 83 | fontWeight = FontWeight.Bold 84 | ) 85 | } 86 | } 87 | } 88 | 89 | @Composable 90 | @Preview(showBackground = true, showSystemUi = true) 91 | private fun Preview() { 92 | FunctionCard( 93 | color = AppTheme.colors.card, 94 | icon = R.drawable.ic_analyze, 95 | title = "数据分析" 96 | ) 97 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | accompanistPermissions = "0.35.1-alpha" 3 | agp = "8.5.0" 4 | bcprovJdk15on = "1.68" 5 | coil = "2.2.2" 6 | coilCompose = "2.6.0" 7 | composeCharts = "0.0.16" 8 | composeviews = "1.7.0.1" 9 | composeWheelPicker = "1.0.0-beta05" 10 | converterKotlinxSerialization = "2.11.0" 11 | kotlin = "1.9.22" 12 | coreKtx = "1.13.1" 13 | junit = "4.13.2" 14 | junitVersion = "1.1.5" 15 | espressoCore = "3.5.1" 16 | kotlinxSerializationJson = "1.5.0" 17 | lifecycleRuntimeKtx = "2.8.4" 18 | activityCompose = "1.9.1" 19 | composeBom = "2024.06.00" 20 | navigationCompose = "2.7.7" 21 | retrofit = "2.11.0" 22 | roomGradlePlugin = "2.7.0-alpha05" 23 | lifecycleRuntimeAndroid = "2.8.7" 24 | 25 | [libraries] 26 | accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } 27 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 28 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } 29 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomGradlePlugin" } 30 | androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomGradlePlugin" } 31 | bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bcprovJdk15on" } 32 | coil = { module = "io.coil-kt:coil", version.ref = "coil" } 33 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } 34 | coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coilCompose" } 35 | compose-charts = { module = "io.github.ehsannarmani:compose-charts", version.ref = "composeCharts" } 36 | compose-wheel-picker = { module = "com.github.zj565061763:compose-wheel-picker", version.ref = "composeWheelPicker" } 37 | composeviews = { module = "io.github.ltttttttttttt:ComposeViews", version.ref = "composeviews" } 38 | converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "converterKotlinxSerialization" } 39 | junit = { group = "junit", name = "junit", version.ref = "junit" } 40 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 41 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 42 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 43 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 44 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 45 | androidx-ui = { group = "androidx.compose.ui", name = "ui" } 46 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 47 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 48 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 49 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 50 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } 51 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 52 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } 53 | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } 54 | androidx-lifecycle-runtime-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-android", version.ref = "lifecycleRuntimeAndroid" } 55 | 56 | [plugins] 57 | androidApplication = { id = "com.android.application", version.ref = "agp" } 58 | jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/repository/RealtimeProfitRepository.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.repository 2 | 3 | import com.lemon.mcdevmanager.api.AnalyzeApi 4 | import com.lemon.mcdevmanager.data.common.CookiesStore 5 | import com.lemon.mcdevmanager.data.common.NETEASE_USER_COOKIE 6 | import com.lemon.mcdevmanager.data.global.AppContext 7 | import com.lemon.mcdevmanager.data.netease.income.OneResRealtimeIncomeBean 8 | import com.lemon.mcdevmanager.utils.CookiesExpiredException 9 | import com.lemon.mcdevmanager.utils.NetworkState 10 | import com.lemon.mcdevmanager.utils.UnifiedExceptionHandler 11 | import com.orhanobut.logger.Logger 12 | import java.text.SimpleDateFormat 13 | import java.util.Calendar 14 | import java.util.Date 15 | import java.util.Locale 16 | 17 | class RealtimeProfitRepository { 18 | companion object { 19 | @Volatile 20 | private var instance: RealtimeProfitRepository? = null 21 | fun getInstance() = instance ?: synchronized(this) { 22 | instance ?: RealtimeProfitRepository().also { instance = it } 23 | } 24 | } 25 | 26 | suspend fun getOneDayDetail( 27 | platform: String, 28 | iid: String, 29 | date: String 30 | ): NetworkState { 31 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 32 | cookie?.let { 33 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 34 | 35 | val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA) 36 | val dateDate = formatter.format(Date(formatter.parse(date).time - 86400000)) 37 | return UnifiedExceptionHandler.handleSuspend { 38 | AnalyzeApi.create().getOneResRealtimeIncome( 39 | platform = if (platform == "pe") "pe" else "comp", 40 | iid = iid, 41 | beginTime = dateDate + "T16:00:00.000Z", 42 | endTime = date + "T15:59:59.999Z" 43 | ) 44 | } 45 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 46 | } 47 | 48 | suspend fun getOneMonthDetail( 49 | platform: String, 50 | iid: String, 51 | year: Int, 52 | month: Int 53 | ): NetworkState { 54 | val cookie = AppContext.cookiesStore[AppContext.nowNickname] 55 | cookie?.let { 56 | CookiesStore.addCookie(NETEASE_USER_COOKIE, cookie) 57 | 58 | val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) 59 | // 上个月底 - 9 天 60 | val calLast = Calendar.getInstance().apply { 61 | set(Calendar.YEAR, year) 62 | set(Calendar.MONTH, month - 2) 63 | set(Calendar.DAY_OF_MONTH, getActualMaximum(Calendar.DAY_OF_MONTH)) 64 | add(Calendar.DAY_OF_MONTH, -9) 65 | } 66 | val lastMonthResult = dateFormat.format(calLast.time) 67 | // 这个月底 - 9 天 68 | val calCurrent = Calendar.getInstance().apply { 69 | set(Calendar.YEAR, year) 70 | set(Calendar.MONTH, month - 1) 71 | set(Calendar.DAY_OF_MONTH, getActualMaximum(Calendar.DAY_OF_MONTH)) // 当月最后一天 72 | add(Calendar.DAY_OF_MONTH, -9) 73 | } 74 | val currentMonthResult = dateFormat.format(calCurrent.time) 75 | 76 | return UnifiedExceptionHandler.handleSuspend { 77 | AnalyzeApi.create().getOneResRealtimeIncome( 78 | platform = if (platform == "pe") "pe" else "comp", 79 | iid = iid, 80 | beginTime = lastMonthResult + "T16:00:00.000Z", 81 | endTime = currentMonthResult + "T15:59:59.999Z" 82 | ) 83 | } 84 | } ?: return NetworkState.Error("无法获取用户cookie, 请重新登录", CookiesExpiredException) 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/LoadingShimmerWidget.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.RepeatMode 5 | import androidx.compose.animation.core.animateFloat 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.rememberInfiniteTransition 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.foundation.background 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.composed 16 | import androidx.compose.ui.geometry.Offset 17 | import androidx.compose.ui.graphics.Brush 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | 22 | fun Modifier.shimmerLoadingAnimation( 23 | isLoadingCompleted: Boolean = true, 24 | isLightModeActive: Boolean = true, 25 | widthOfShadowBrush: Int = 500, 26 | angleOfAxisY: Float = 270f, 27 | durationMillis: Int = 1000, 28 | ): Modifier { 29 | if (isLoadingCompleted) { 30 | return this 31 | } 32 | else { 33 | return composed { 34 | val shimmerColors = ShimmerAnimationData(isLightMode = isLightModeActive).getColours() 35 | 36 | val transition = rememberInfiniteTransition(label = "") 37 | 38 | val translateAnimation = transition.animateFloat( 39 | initialValue = 0f, 40 | targetValue = (durationMillis + widthOfShadowBrush).toFloat(), 41 | animationSpec = infiniteRepeatable( 42 | animation = tween( 43 | durationMillis = durationMillis, 44 | easing = LinearEasing, 45 | ), 46 | repeatMode = RepeatMode.Restart, 47 | ), 48 | label = "Shimmer loading animation", 49 | ) 50 | 51 | this.background( 52 | brush = Brush.linearGradient( 53 | colors = shimmerColors, 54 | start = Offset(x = translateAnimation.value - widthOfShadowBrush, y = 0.0f), 55 | end = Offset(x = translateAnimation.value, y = angleOfAxisY), 56 | ), 57 | ) 58 | } 59 | } 60 | } 61 | 62 | data class ShimmerAnimationData( 63 | private val isLightMode: Boolean 64 | ) { 65 | fun getColours(): List { 66 | return if (isLightMode) { 67 | val color = Color.White 68 | 69 | listOf( 70 | color.copy(alpha = 0.3f), 71 | color.copy(alpha = 0.5f), 72 | color.copy(alpha = 1.0f), 73 | color.copy(alpha = 0.5f), 74 | color.copy(alpha = 0.3f), 75 | ) 76 | } else { 77 | val color = Color.Black 78 | 79 | listOf( 80 | color.copy(alpha = 0.0f), 81 | color.copy(alpha = 0.3f), 82 | color.copy(alpha = 0.5f), 83 | color.copy(alpha = 0.3f), 84 | color.copy(alpha = 0.0f), 85 | ) 86 | } 87 | } 88 | } 89 | 90 | 91 | @Preview(showBackground = true, showSystemUi = true) 92 | @Composable 93 | private fun LoadingShimmerPreview() { 94 | Box( 95 | modifier = Modifier 96 | // .padding(8.dp) 97 | .fillMaxWidth() 98 | .height(200.dp) 99 | // .clip(RoundedCornerShape(8.dp)) 100 | .background(Color.LightGray) 101 | .shimmerLoadingAnimation(false) 102 | ) 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/AppLoadingWidget.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.ColorFilter 21 | import androidx.compose.ui.platform.LocalConfiguration 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.text.font.Font 24 | import androidx.compose.ui.text.font.FontFamily 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import coil.ImageLoader 29 | import coil.compose.AsyncImage 30 | import coil.decode.GifDecoder 31 | import coil.decode.ImageDecoderDecoder 32 | import com.lemon.mcdevmanager.R 33 | import com.lemon.mcdevmanager.ui.theme.AppTheme 34 | 35 | @Composable 36 | fun AppLoadingWidget(showBackground: Boolean = true) { 37 | val context = LocalContext.current 38 | val configuration = LocalConfiguration.current 39 | 40 | var imageLoaded by remember { mutableStateOf(false) } 41 | 42 | val imageLoader = ImageLoader.Builder(context) 43 | .components { 44 | if (Build.VERSION.SDK_INT >= 28) { 45 | add(ImageDecoderDecoder.Factory()) 46 | } else { 47 | add(GifDecoder.Factory()) 48 | } 49 | } 50 | .build() 51 | 52 | val height = configuration.screenHeightDp.dp 53 | val width = configuration.screenWidthDp.dp 54 | val minSize = if (height > width) width else height 55 | 56 | Box(modifier = Modifier.fillMaxSize()) { 57 | if (showBackground)Box( 58 | modifier = Modifier 59 | .fillMaxSize() 60 | .background(Color.Black.copy(alpha = 0.35f)) 61 | ) 62 | Box( 63 | modifier = Modifier 64 | .size((minSize * 0.45f)) 65 | .align(Alignment.Center) 66 | ) { 67 | AsyncImage( 68 | model = R.drawable.loading, 69 | contentDescription = "loading", 70 | imageLoader = imageLoader, 71 | modifier = Modifier 72 | .fillMaxSize() 73 | .clip(RoundedCornerShape(8.dp)) 74 | .align(Alignment.Center), 75 | colorFilter = ColorFilter.lighting( 76 | multiply = AppTheme.colors.imgTintColor, 77 | add = Color.Transparent 78 | ), 79 | onSuccess = { 80 | imageLoaded = true 81 | } 82 | ) 83 | if (imageLoaded) 84 | Text( 85 | text = "Loading...", 86 | color = Color.White, 87 | fontFamily = FontFamily(Font(R.font.minecraft_ae)), 88 | modifier = Modifier 89 | .align(Alignment.BottomCenter) 90 | .align(Alignment.BottomCenter) 91 | .padding(bottom = 10.dp), 92 | fontSize = 16.sp, 93 | ) 94 | } 95 | } 96 | } 97 | 98 | @Preview(showBackground = true) 99 | @Composable 100 | private fun AppLoadingWidgetPreview() { 101 | AppLoadingWidget() 102 | } -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/MyDiskLogStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.os.Message 6 | import java.io.File 7 | import java.io.FileWriter 8 | import java.io.IOException 9 | 10 | /** 11 | * Abstract class that takes care of background threading the file log operation on Android. 12 | * implementing classes are free to directly perform I/O operations there. 13 | * 14 | * 15 | * Writes all logs to the disk with CSV format. 16 | */ 17 | class MyDiskLogStrategy(handler: Handler) : LogStrategy { 18 | private val handler = Utils.checkNotNull(handler) 19 | 20 | override fun log(level: Int, tag: String?, message: String) { 21 | Utils.checkNotNull(message) 22 | 23 | // do nothing on the calling thread, simply pass the tag/msg to the background thread 24 | handler.sendMessage(handler.obtainMessage(level, message)) 25 | } 26 | 27 | internal class WriteHandler( 28 | looper: Looper, 29 | folder: String, 30 | fileName: String, 31 | private val maxFileSize: Int 32 | ) : Handler( 33 | Utils.checkNotNull(looper) 34 | ) { 35 | private val folder = Utils.checkNotNull(folder) 36 | private var mFileName = "sqn_log.log" 37 | 38 | init { 39 | mFileName = fileName 40 | } 41 | 42 | override fun handleMessage(msg: Message) { 43 | val content = msg.obj as String 44 | 45 | var fileWriter: FileWriter? = null 46 | val logFile = getLogFile(folder, "logs") 47 | 48 | try { 49 | fileWriter = FileWriter(logFile, true) 50 | 51 | writeLog(fileWriter, content) 52 | 53 | fileWriter.flush() 54 | fileWriter.close() 55 | } catch (e: IOException) { 56 | if (fileWriter != null) { 57 | try { 58 | fileWriter.flush() 59 | fileWriter.close() 60 | } catch (e1: IOException) { /* fail silently */ 61 | } 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * This is always called on a single background thread. 68 | * Implementing classes must ONLY write to the fileWriter and nothing more. 69 | * The abstract class takes care of everything else including close the stream and catching IOException 70 | * 71 | * @param fileWriter an instance of FileWriter already initialised to the correct file 72 | */ 73 | @Throws(IOException::class) 74 | private fun writeLog(fileWriter: FileWriter, content: String) { 75 | Utils.checkNotNull(fileWriter) 76 | Utils.checkNotNull(content) 77 | 78 | fileWriter.append(content) 79 | } 80 | 81 | private fun getLogFile(folderName: String, fileName: String): File { 82 | Utils.checkNotNull(folderName) 83 | Utils.checkNotNull(fileName) 84 | 85 | val folder = File(folderName) 86 | if (!folder.exists()) { 87 | //TODO: What if folder is not created, what happens then? 88 | folder.mkdirs() 89 | } 90 | 91 | // File existingFile = null; 92 | 93 | // int newFileCount = 0; 94 | val newFile = File(folder, mFileName) 95 | 96 | // while (newFile.exists()) { 97 | // existingFile = newFile; 98 | // newFileCount++; 99 | // newFile = new File(folder, mFileName); 100 | // } 101 | 102 | // if (existingFile != null) { 103 | // if (existingFile.length() >= maxFileSize) { 104 | // return newFile; 105 | // } 106 | // return existingFile; 107 | // } 108 | return newFile 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/ui/widget/FlowTabWidget.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.ui.widget 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.shape.CircleShape 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material.ripple 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.draw.clip 21 | import androidx.compose.ui.res.painterResource 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import com.lemon.mcdevmanager.R 26 | import com.lemon.mcdevmanager.ui.theme.AppTheme 27 | import com.lemon.mcdevmanager.ui.theme.TextWhite 28 | import com.lt.compose_views.other.HorizontalSpace 29 | 30 | @Composable 31 | fun FlowTabWidget( 32 | modifier: Modifier = Modifier, 33 | text: String = "", 34 | isSelected: Boolean = false, 35 | isShowDelete: Boolean = false, 36 | onDeleteClick: () -> Unit = {}, 37 | onClick: (Boolean) -> Unit = {} 38 | ) { 39 | Box( 40 | modifier = Modifier 41 | .padding(start = 8.dp, top = 8.dp) 42 | .then(modifier) 43 | ) { 44 | Box( 45 | modifier = Modifier 46 | .clip(RoundedCornerShape(8.dp)) 47 | .background(color = if (isSelected) AppTheme.colors.primaryColor else AppTheme.colors.card) 48 | .border( 49 | width = 1.dp, 50 | color = AppTheme.colors.primaryColor, 51 | shape = RoundedCornerShape(8.dp) 52 | ) 53 | .clickable( 54 | interactionSource = remember { MutableInteractionSource() }, 55 | indication = ripple() 56 | ) { 57 | onClick(isSelected) 58 | } 59 | .padding(8.dp) 60 | .align(Alignment.Center) 61 | ) { 62 | Row { 63 | Text( 64 | text = text, 65 | color = if (isSelected) TextWhite else AppTheme.colors.textColor, 66 | fontSize = 14.sp, 67 | modifier = Modifier.align( 68 | Alignment.CenterVertically 69 | ) 70 | ) 71 | if (isShowDelete) { 72 | HorizontalSpace(dp = 8.dp) 73 | Box( 74 | modifier = Modifier 75 | .size(20.dp) 76 | .clip(CircleShape) 77 | .background(AppTheme.colors.primaryColor) 78 | .clickable { 79 | onDeleteClick() 80 | } 81 | ) { 82 | Image( 83 | painter = painterResource(id = R.drawable.ic_close), 84 | contentDescription = "delete", 85 | modifier = Modifier.padding(4.dp) 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | @Composable 95 | @Preview 96 | private fun PreviewFlowTabWidget() { 97 | FlowTabWidget( 98 | text = "FlowTabWidget", 99 | isSelected = false, 100 | isShowDelete = true 101 | ) {} 102 | } -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/DiskLogStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.os.Message 6 | import java.io.File 7 | import java.io.FileWriter 8 | import java.io.IOException 9 | 10 | /** 11 | * Abstract class that takes care of background threading the file log operation on Android. 12 | * implementing classes are free to directly perform I/O operations there. 13 | * 14 | * 15 | * Writes all logs to the disk with CSV format. 16 | */ 17 | class DiskLogStrategy(handler: Handler) : LogStrategy { 18 | private val handler = Utils.checkNotNull(handler) 19 | 20 | override fun log(level: Int, tag: String?, message: String) { 21 | Utils.checkNotNull(message) 22 | 23 | // do nothing on the calling thread, simply pass the tag/msg to the background thread 24 | // TODO 从这里写入日志 25 | handler.sendMessage(handler.obtainMessage(level, message)) 26 | } 27 | 28 | internal class WriteHandler( 29 | looper: Looper, 30 | folder: String, 31 | fileName: String, 32 | private val maxFileSize: Int 33 | ) : Handler( 34 | Utils.checkNotNull(looper) 35 | ) { 36 | private val folder = Utils.checkNotNull(folder) 37 | private var mFileName = "mcDevMng_log.log" 38 | 39 | init { 40 | mFileName = fileName 41 | } 42 | 43 | override fun handleMessage(msg: Message) { 44 | val content = msg.obj as String 45 | 46 | var fileWriter: FileWriter? = null 47 | val logFile = getLogFile(folder, mFileName) 48 | 49 | try { 50 | fileWriter = FileWriter(logFile, true) 51 | writeLog(fileWriter, content) 52 | fileWriter.flush() 53 | fileWriter.close() 54 | } catch (e: IOException) { 55 | if (fileWriter != null) { 56 | try { 57 | fileWriter.flush() 58 | fileWriter.close() 59 | } catch (e1: IOException) { /* fail silently */ 60 | } 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * This is always called on a single background thread. 67 | * Implementing classes must ONLY write to the fileWriter and nothing more. 68 | * The abstract class takes care of everything else including close the stream and catching IOException 69 | * 70 | * @param fileWriter an instance of FileWriter already initialised to the correct file 71 | */ 72 | @Throws(IOException::class) 73 | private fun writeLog(fileWriter: FileWriter, content: String) { 74 | Utils.checkNotNull(fileWriter) 75 | Utils.checkNotNull(content) 76 | 77 | fileWriter.append(content) 78 | } 79 | 80 | private fun getLogFile(folderName: String, fileName: String): File { 81 | Utils.checkNotNull(folderName) 82 | Utils.checkNotNull(fileName) 83 | 84 | val folder = File(folderName) 85 | if (!folder.exists()) { 86 | //TODO: What if folder is not created, what happens then? 87 | folder.mkdirs() 88 | } 89 | 90 | // File existingFile = null; 91 | 92 | // int newFileCount = 0; 93 | val newFile = File(folder, fileName) 94 | 95 | // while (newFile.exists()) { 96 | // existingFile = newFile; 97 | // newFileCount++; 98 | // newFile = new File(folder, mFileName); 99 | // } 100 | 101 | // if (existingFile != null) { 102 | // if (existingFile.length() >= maxFileSize) { 103 | // return newFile; 104 | // } 105 | // return existingFile; 106 | // } 107 | return newFile 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/common/ConstantValue.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.common 2 | 3 | // navigation 地址 4 | const val SPLASH_PAGE = "SPLASH_PAGE" 5 | const val LOGIN_PAGE = "LOGIN_PAGE" 6 | const val MAIN_PAGE = "MAIN_PAGE" 7 | const val FEEDBACK_PAGE = "FEEDBACK_PAGE" 8 | const val COMMENT_PAGE = "COMMENT_PAGE" 9 | const val ANALYZE_PAGE = "ANALYZE_PAGE" 10 | const val REALTIME_PROFIT_PAGE = "REALTIME_PROFIT_PAGE" 11 | const val SETTING_PAGE = "SETTING_PAGE" 12 | const val LOG_PAGE = "LOG_PAGE" 13 | const val ABOUT_PAGE = "ABOUT_PAGE" 14 | const val OPEN_SOURCE_INFO_PAGE = "OPEN_SOURCE_INFO_PAGE" 15 | const val LICENSE_PAGE = "LICENSE_PAGE" 16 | const val PROFIT_PAGE = "PROFIT_PAGE" 17 | const val INCENTIVE_PAGE = "INCENTIVE_PAGE" 18 | const val INCOME_DETAIL_PAGE = "INCOME_DETAIL_PAGE" 19 | const val ALL_MOD_SELECT_PAGE = "ALL_MOD_SELECT_PAGE" 20 | const val MOD_DATA_DETAIL_PAGE = "MOD_DATA_DETAIL_PAGE" 21 | 22 | // 网易用户cookie名 23 | const val NETEASE_USER_COOKIE = "NTES_SESS" 24 | 25 | // 网易登录接口参数 26 | const val pkid = "kBSLIYY" 27 | const val pd = "x19_developer" 28 | const val pkht = "mcdev.webapp.163.com" 29 | const val channel = 0 30 | 31 | // 网易加密密钥 32 | const val SM4Key = "BC60B8B9E4FFEFFA219E5AD77F11F9E2" 33 | const val RSAKey = 34 | "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5gsH+AA4XWONB5TDcUd+xCz7ejOFHZKlcZDx+pF1i7Gsvi1vjyJoQhRtRSn950x498VUkx7rUxg1/ScBVfrRxQOZ8xFBye3pjAzfb22+RCuYApSVpJ3OO3KsEuKExftz9oFBv3ejxPlYc5yq7YiBO8XlTnQN0Sa4R4qhPO3I2MQIDAQAB" 35 | 36 | // base64图片 37 | const val UPImage = 38 | "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAOCAYAAAAWo42rAAAAOklEQVQokWP87+vAgAb+Q7mMyMJM6KpwARYk8f9oalBMJslEdJOwupkkE5F9h246XI5oE0egQgYGBgC7yAesshAnhAAAAABJRU5ErkJggg==" 39 | const val NORMALImage = 40 | "iVBORw0KGgoAAAANSUhEUgAAAAwAAAACCAIAAADjHarAAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4FpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ4IDc5LjE2NDAzNiwgMjAxOS8wOC8xMy0wMTowNjo1NyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpmMDNhZTQyMy0zMWIyLWEyNGItOTBkOC0yNDk2ZDEzNDkxZmYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OEFFQUQ1QUM5MUQ1MTFFQjg3MTBDNTVDOUQ5NDQ0NTkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OEFFQUQ1QUI5MUQ1MTFFQjg3MTBDNTVDOUQ5NDQ0NTkiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIxLjAgKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6M2YzNDcyODQtYzYyNC0xMjQ4LWE1NDYtYTQzNWJlYTUzYTRhIiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YjNiNTc5ODctN2EwOS1lOTQ0LWJjYzItM2FlYjlhZTc3NGQ5Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+uOh2awAAABVJREFUeNpiPDo7g4EQYGIgAgAEGACBewHMKEdvQQAAAABJRU5ErkJggg==" 41 | const val DOWNImage = 42 | "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAOCAYAAAAWo42rAAAAOklEQVQokWNMPBzKgAT+M6ACRhiPiYFIMDIVsmAJO6zhSpKJsNDHZTJYniQTUXQimQyPZ+JNZGBgAAA7SAabRFT2cAAAAABJRU5ErkJggg==" 43 | const val scoreImage = 44 | "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAKnSURBVHgBpZRLSFRRGMf/55x774wzoSPplJNaiTcsQzIIpqSCLCOhFoHuaiG1rUWbsCQMilq0iLRFi6iFUEwubNd7UZuKBrEwMgOzTHJ8TfO8M/fROZc0Z3DSxj/87j0vzvl/33fuBbIkKfQifyXmIIR8aTpW58YyJS0yJnOc8z0CBf8hsqB9g3P0tNdbeLKkZFVE0xCKxTBhGDgRCon5FEfljGKZDj0cX9qysBIJh3fFxpd9vl1lsrxhvX8rKv1boA3/QOLxG0Rkirc714EQmN0P3z0dmQhDKWC3UgmjN5fDVo6zTFGww+WC7C0Gq/JBnonCkmVIbgV1G71iLU2k9CbbBaN9gLGoQ8Y5LzZuLCiwYxamWTgGbXYWEQ9DotSNqQJAS6WhyBI2V5aAWiDjU1GRTxNZOZ3PYZQXYUbkb2gUyrcQklXF+OWvgM6LEpmcttcc8auQGMO13tfNvCsQhXqZsSGV6X1GiCOYSjX/NIzCCidBuYPBMONIT81kXINkUgOlxD4kl6SO9o62wGBA6n3w8bllWQ3bikzUuylcKROeER2SxFC5drW9OByN2W8Rfi7Rzs5Os7al1iDEGuL9T6aJyYimI5k2uBMTBkfXjQxMy8ztUDwCrQERQ5toD3yPtHMuVa9xYbeqgzEKK+tuxhPavzfMV5RSqaajJuPTJIusK+J4ilz0oGGQbnGocLlQZ88cx94929Fz78nszdt9kaUchgXhuDn9dyizqorTgXJfKRSH7OHp8Czl0FZhuaMacXrY4nFlz+3z1x9SJNaoqpugqir6BwZxvesOVqKrHOvCuVOWlRi2XjzqEVWzYchPBzgN/IeBka9jCPZ/GA++H+wydfMZ8pTtcA7FJQdbruwXxczv2vC0jvFivMKf7EqMfHYyqov2bwn8AbL9qKdHAAAAAElFTkSuQmCC" -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/data/netease/resource/ResourceBean.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.data.netease.resource 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ResourceBean( 8 | @SerialName("create_time") 9 | val createTime: String, 10 | @SerialName("item_id") 11 | val itemId: String, 12 | @SerialName("item_name") 13 | val itemName: String, 14 | @SerialName("online_time") 15 | val onlineTime: String = "UNKNOWN", 16 | @SerialName("pri_type") 17 | val priType: Int, 18 | val price: Int 19 | ) 20 | 21 | @Serializable 22 | data class ResourceResponseBean( 23 | val count: Int, 24 | val item: List 25 | ) 26 | 27 | 28 | @Serializable 29 | data class ResDetailBean( 30 | @SerialName("DAU") 31 | val dau: Int, 32 | @SerialName("cnt_buy") 33 | val cntBuy: Int, 34 | @SerialName("dateid") 35 | val dateId: String, 36 | @SerialName("diamond") 37 | val diamond: Int, 38 | @SerialName("download_num") 39 | val downloadNum: Int = 0, 40 | @SerialName("iid") 41 | val iid: String, 42 | @SerialName("platform") 43 | val platform: String, 44 | @SerialName("points") 45 | val points: Int, 46 | @SerialName("refund_rate") 47 | val refundRate: Double, 48 | @SerialName("res_name") 49 | val resName: String, 50 | @SerialName("upload_time") 51 | val uploadTime: String 52 | ) 53 | 54 | @Serializable 55 | data class ResMonthDetailBean( 56 | @SerialName("avg_dau") 57 | val avgDau: Int, 58 | @SerialName("avg_day_buy") 59 | val avgDayBuy: Int, 60 | @SerialName("download_num") 61 | val downloadNum: Int, 62 | @SerialName("iid") 63 | val iid: String, 64 | @SerialName("mau") 65 | val mau: Int, 66 | @SerialName("monthid") 67 | val monthId: String, 68 | @SerialName("platform") 69 | val platform: String, 70 | @SerialName("res_name") 71 | val resName: String, 72 | @SerialName("total_diamond") 73 | val totalDiamond: Int, 74 | @SerialName("total_points") 75 | val totalPoints: Int, 76 | @SerialName("upload_time") 77 | val uploadTime: String = "UNKNOWN" 78 | ) 79 | 80 | @Serializable 81 | data class NewResDetailBean( 82 | @SerialName("DAU") 83 | val dau: Int, 84 | @SerialName("avg_first_type_buy") 85 | val avgFirstTypeBuy: Double, 86 | @SerialName("avg_first_type_diamond") 87 | val avgFirstTypeDiamond: Double, 88 | @SerialName("avg_first_type_focus") 89 | val avgFirstTypeFocus: Double, 90 | @SerialName("avg_first_type_role_play") 91 | val avgFirstTypeRolePlay: Double, 92 | @SerialName("avg_playtime") 93 | val avgPlaytime: Double, 94 | @SerialName("avg_total_first_type_buy") 95 | val avgTotalFirstTypeBuy: Double, 96 | @SerialName("cnt_buy") 97 | val cntBuy: Int, 98 | @SerialName("dateid") 99 | val dateId: String, 100 | val diamond: Int, 101 | @SerialName("download_num") 102 | val downloadNum: Int, 103 | @SerialName("first_type_avg_role_time") 104 | val firstTypeAvgRoleTime: Double, 105 | @SerialName("focus_cnt") 106 | val focusCnt: Int, 107 | val iid: String, 108 | @SerialName("pass_avg_role_time_ratio") 109 | val passAvgRoleTimeRatio: Double, 110 | @SerialName("pass_buy_cnt_ratio") 111 | val passBuyCntRatio: Double, 112 | @SerialName("pass_cnt_role_play_ratio") 113 | val passCntRolePlayRatio: Double, 114 | @SerialName("pass_focus_cnt_ratio") 115 | val passFocusCntRatio: Double, 116 | @SerialName("pass_pay_diamond_ratio") 117 | val passPayDiamondRatio: Double, 118 | val platform: String, 119 | val points: Int, 120 | @SerialName("refund_rate") 121 | val refundRate: Double, 122 | @SerialName("res_name") 123 | val resName: String, 124 | @SerialName("star_adjusted") 125 | val starAdjusted: Double, 126 | @SerialName("upload_time") 127 | val uploadTime: String 128 | ) 129 | 130 | @Serializable 131 | data class NewResDetailResponseBean( 132 | val data: List 133 | ) 134 | 135 | @Serializable 136 | data class ResDetailResponseBean( 137 | val data: List 138 | ) 139 | 140 | @Serializable 141 | data class ResMonthDetailResponseBean( 142 | val data: List 143 | ) -------------------------------------------------------------------------------- /logger/src/main/java/com/orhanobut/logger/CsvFormatStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.orhanobut.logger 2 | 3 | import android.os.Handler 4 | import android.os.HandlerThread 5 | import java.text.SimpleDateFormat 6 | import java.util.Date 7 | import java.util.Locale 8 | 9 | /** 10 | * CSV formatted file logging for Android. 11 | * Writes to CSV the following data: 12 | * epoch timestamp, ISO8601 timestamp (human-readable), log level, tag, log message. 13 | */ 14 | class CsvFormatStrategy private constructor(builder: Builder) : FormatStrategy { 15 | private val date: Date 16 | private val dateFormat: SimpleDateFormat 17 | private val logStrategy: LogStrategy 18 | private val tag: String? 19 | 20 | init { 21 | Utils.checkNotNull(builder) 22 | 23 | date = builder.date!! 24 | dateFormat = builder.dateFormat!! 25 | logStrategy = builder.logStrategy!! 26 | tag = builder.tag 27 | } 28 | 29 | override fun log(priority: Int, onceOnlyTag: String?, message: String) { 30 | var messageLog = message 31 | Utils.checkNotNull(messageLog) 32 | 33 | val tag = formatTag(onceOnlyTag) 34 | 35 | date.time = System.currentTimeMillis() 36 | 37 | val builder = StringBuilder() 38 | 39 | // machine-readable date/time 40 | builder.append(date.time.toString()) 41 | 42 | // human-readable date/time 43 | builder.append(SEPARATOR) 44 | builder.append(dateFormat.format(date)) 45 | 46 | // level 47 | builder.append(SEPARATOR) 48 | builder.append(Utils.logLevel(priority)) 49 | 50 | // tag 51 | builder.append(SEPARATOR) 52 | builder.append(tag) 53 | 54 | // message 55 | if (messageLog.contains(NEW_LINE)) { 56 | // a new line would break the CSV format, so we replace it here 57 | messageLog = messageLog.replace(NEW_LINE.toRegex(), NEW_LINE_REPLACEMENT) 58 | } 59 | builder.append(SEPARATOR) 60 | builder.append(messageLog) 61 | 62 | // new line 63 | builder.append(NEW_LINE) 64 | 65 | logStrategy.log(priority, tag, builder.toString()) 66 | } 67 | 68 | private fun formatTag(tag: String?): String? { 69 | if (!Utils.isEmpty(tag) && !Utils.equals(this.tag, tag)) { 70 | return this.tag + "-" + tag 71 | } 72 | return this.tag 73 | } 74 | 75 | class Builder { 76 | var date: Date? = null 77 | var dateFormat: SimpleDateFormat? = null 78 | var logStrategy: LogStrategy? = null 79 | var tag: String? = "PRETTY_LOGGER" 80 | 81 | fun date(`val`: Date?): Builder { 82 | date = `val` 83 | return this 84 | } 85 | 86 | fun dateFormat(`val`: SimpleDateFormat?): Builder { 87 | dateFormat = `val` 88 | return this 89 | } 90 | 91 | fun logStrategy(`val`: LogStrategy?): Builder { 92 | logStrategy = `val` 93 | return this 94 | } 95 | 96 | fun tag(tag: String?): Builder { 97 | this.tag = tag 98 | return this 99 | } 100 | 101 | fun build(fileName: String?, diskPath: String): CsvFormatStrategy { 102 | if (date == null) { 103 | date = Date() 104 | } 105 | if (dateFormat == null) { 106 | dateFormat = SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.CHINA) 107 | } 108 | if (logStrategy == null) { 109 | val ht = HandlerThread("AndroidFileLogger.$diskPath") 110 | ht.start() 111 | val handler: Handler = 112 | DiskLogStrategy.WriteHandler(ht.looper, diskPath, fileName!!, MAX_BYTES) 113 | logStrategy = DiskLogStrategy(handler) 114 | } 115 | return CsvFormatStrategy(this) 116 | } 117 | 118 | companion object { 119 | private const val MAX_BYTES = 5000 * 1024 // 500K averages to a 4000 lines per file 120 | } 121 | } 122 | 123 | companion object { 124 | private val NEW_LINE: String = System.lineSeparator() 125 | private val NEW_LINE_REPLACEMENT: String = System.lineSeparator() 126 | private const val SEPARATOR = "," 127 | 128 | fun newBuilder(): Builder { 129 | return Builder() 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/com/lemon/mcdevmanager/api/AnalyzeApi.kt: -------------------------------------------------------------------------------- 1 | package com.lemon.mcdevmanager.api 2 | 3 | import com.lemon.mcdevmanager.data.AddCookiesInterceptor 4 | import com.lemon.mcdevmanager.data.CommonInterceptor 5 | import com.lemon.mcdevmanager.data.common.JSONConverter 6 | import com.lemon.mcdevmanager.data.common.NETEASE_MC_DEV_LINK 7 | import com.lemon.mcdevmanager.data.netease.income.OneResRealtimeIncomeBean 8 | import com.lemon.mcdevmanager.data.netease.resource.NewResDetailBean 9 | import com.lemon.mcdevmanager.data.netease.resource.NewResDetailResponseBean 10 | import com.lemon.mcdevmanager.data.netease.resource.ResDetailResponseBean 11 | import com.lemon.mcdevmanager.data.netease.resource.ResMonthDetailResponseBean 12 | import com.lemon.mcdevmanager.data.netease.resource.ResourceResponseBean 13 | import com.lemon.mcdevmanager.utils.ResponseData 14 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 15 | import okhttp3.OkHttpClient 16 | import retrofit2.Retrofit 17 | import retrofit2.converter.kotlinx.serialization.asConverterFactory 18 | import retrofit2.http.GET 19 | import retrofit2.http.Path 20 | import retrofit2.http.Query 21 | import java.util.concurrent.TimeUnit 22 | 23 | interface AnalyzeApi { 24 | @GET("/items/categories/{platform}/") 25 | suspend fun getAllResource( 26 | @Path("platform") platform: String = "pe", 27 | @Query("start") start: Int = 0, 28 | @Query("span") span: Int = Int.MAX_VALUE 29 | ): ResponseData 30 | 31 | @GET("/data_analysis/day_detail/") 32 | suspend fun getDayDetail( 33 | @Query("platform") platform: String, 34 | @Query("category") category: String, 35 | @Query("start_date") startDate: String, 36 | @Query("end_date") endDate: String, 37 | @Query("item_list_str") itemListStr: String, 38 | @Query("sort") sort: String = "dateid", 39 | @Query("order") order: String = "ASC", 40 | @Query("start") start: Int = 0, 41 | @Query("span") span: Int = Int.MAX_VALUE 42 | ): ResponseData 43 | 44 | @GET("/data_analysis/day_detail/") 45 | suspend fun getNewDayDetail( 46 | @Query("platform") platform: String, 47 | @Query("category") category: String, 48 | @Query("start_date") startDate: String, 49 | @Query("end_date") endDate: String, 50 | @Query("item_list_str") itemListStr: String, 51 | @Query("sort") sort: String = "dateid", 52 | @Query("order") order: String = "ASC", 53 | @Query("start") start: Int = 0, 54 | @Query("span") span: Int = Int.MAX_VALUE, 55 | @Query("is_need_us_rank_data") isNeedUsRankData: Boolean = true 56 | ): ResponseData 57 | 58 | @GET("/data_analysis/month_detail/") 59 | suspend fun getMonthDetail( 60 | @Query("platform") platform: String, 61 | @Query("category") category: String, 62 | @Query("start_date") startDate: String, 63 | @Query("end_date") endDate: String, 64 | @Query("sort") sort: String = "monthid", 65 | @Query("order") order: String = "DESC", 66 | @Query("start") start: Int = 0, 67 | @Query("span") span: Int = Int.MAX_VALUE, 68 | @Query("day_sort") daySort: String = "cnt_buy", 69 | @Query("day_span") daySpan: Int = Int.MAX_VALUE, 70 | @Query("day_dateid") dayDateId: String 71 | ): ResponseData 72 | 73 | @GET("/items/categories/{platform}/{iid}/incomes/") 74 | suspend fun getOneResRealtimeIncome( 75 | @Path("platform") platform: String, 76 | @Path("iid") iid: String, 77 | @Query("begin_time") beginTime: String, 78 | @Query("end_time") endTime: String 79 | ): ResponseData 80 | 81 | companion object { 82 | /** 83 | * 获取接口实例用于调用对接方法 84 | * @return ServerApi 85 | */ 86 | fun create(): AnalyzeApi { 87 | val client = OkHttpClient.Builder().connectTimeout(15, TimeUnit.SECONDS) 88 | .readTimeout(15, TimeUnit.SECONDS).addInterceptor(AddCookiesInterceptor()) 89 | .addInterceptor(CommonInterceptor()).build() 90 | return Retrofit.Builder().baseUrl(NETEASE_MC_DEV_LINK).addConverterFactory( 91 | JSONConverter.asConverterFactory( 92 | "application/json; charset=UTF8".toMediaTypeOrNull()!! 93 | ) 94 | ).client(client).build().create(AnalyzeApi::class.java) 95 | } 96 | } 97 | } --------------------------------------------------------------------------------