()
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 | }
--------------------------------------------------------------------------------