diff --git a/.gitignore b/.gitignore index 4b8b34d..20cae7a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,5 @@ local.properties /app/src/androidTest/java/com/lyneon/cytoidinfoquerier/ /app/src/test/java/com/lyneon/cytoidinfoquerier/ /.idea/deploymentTargetDropDown.xml -/app/src/main/java/com/lyneon/cytoidinfoquerier/SecretData.kt /.idea/ +/app/src/main/java/com/lyneon/cytoidinfoquerier/Secret.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b6d9ce..d7f1078 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.lyneon.cytoidinfoquerier" minSdk = 24 targetSdk = 34 - versionCode = 5 - versionName = "1.3.1" + versionCode = 8 + versionName = "1.4.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -29,7 +29,7 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -58,33 +58,37 @@ android { } dependencies { - val navVersion = "2.7.5" - val appCenterSdkVersion = "5.0.3" + val navVersion = "2.7.7" + val appCenterSdkVersion = "5.0.4" + val media3Version = "1.3.0" + val composeBomVersion = "2024.02.02" implementation("com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}") implementation("com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}") implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("io.coil-kt:coil-compose:2.5.0") - implementation("com.tencent:mmkv:1.3.2") + implementation("com.tencent:mmkv:1.3.3") implementation("androidx.navigation:navigation-compose:$navVersion") + implementation("androidx.compose.material:material-icons-extended:1.6.3") implementation("dev.shreyaspatil:capturable:1.0.3") + implementation("com.patrykandpatrick.vico:compose-m3:1.14.0") + implementation("androidx.media3:media3-exoplayer:$media3Version") implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") - implementation("androidx.activity:activity-compose:1.8.1") - implementation(platform("androidx.compose:compose-bom:2023.10.01")) + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:$composeBomVersion")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") - implementation(platform("androidx.compose:compose-bom:2023.10.01")) + implementation(platform("androidx.compose:compose-bom:$composeBomVersion")) testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01")) + androidTestImplementation(platform("androidx.compose:compose-bom:$composeBomVersion")) androidTestImplementation("androidx.compose.ui:ui-test-junit4") - androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01")) debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 18b722d..faff848 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,10 @@ + + + + .toIntList() = this.map { it.toArgb() } + +fun List.toIntArray() = this.toIntList().toIntArray() \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/CytoidConstant.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/CytoidConstant.kt new file mode 100644 index 0000000..ebdafee --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/CytoidConstant.kt @@ -0,0 +1,7 @@ +package com.lyneon.cytoidinfoquerier.data.constant + +object CytoidConstant { + const val gamePackageName = "me.tigerhix.cytoid" + const val gameAndroidDataPath = "Android/data/$gamePackageName" + const val chartsPath = "$gameAndroidDataPath/files/Cytoid" +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/CytoidScoreRange.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/CytoidScoreRange.kt new file mode 100644 index 0000000..5426bae --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/CytoidScoreRange.kt @@ -0,0 +1,14 @@ +package com.lyneon.cytoidinfoquerier.data.constant + +object CytoidScoreRange { + val max = 1000000 + val sss = 999000 until 1000000 + val ss = 995000 until 999000 + val s = 990000 until 995000 + val aa = 950000 until 990000 + val a = 900000 until 950000 + val b = 800000 until 900000 + val c = 700000 until 800000 + val d = 600000 until 700000 + val f = 0 until 600000 +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/MMKVKeys.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/MMKVKeys.kt new file mode 100644 index 0000000..76fe88f --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/MMKVKeys.kt @@ -0,0 +1,7 @@ +package com.lyneon.cytoidinfoquerier.data.constant + +object MMKVKeys { + const val ENABLE_APP_CENTER = "ENABLE_APP_CENTER" + const val GRID_COLUMNS_COUNT_PORTRAIT = "GRID_COLUMNS_COUNT_PORTRAIT" + const val GRID_COLUMNS_COUNT_LANDSCAPE = "GRID_COLUMNS_COUNT_LANDSCAPE" +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/NavRoute.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/NavRoute.kt similarity index 59% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/NavRoute.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/NavRoute.kt index 0b1c332..3221a23 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/NavRoute.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/NavRoute.kt @@ -1,8 +1,9 @@ -package com.lyneon.cytoidinfoquerier.ui.compose +package com.lyneon.cytoidinfoquerier.data.constant object NavRoute { const val home = "home" const val analytics = "analytics" const val profile = "profile" const val settings = "settings" + const val gridColumnsSetting = "gridColumnsSetting" } diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/RecordQueryOrder.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/RecordQueryOrder.kt similarity index 61% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/RecordQueryOrder.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/RecordQueryOrder.kt index 46d7fe3..1d9e33f 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/RecordQueryOrder.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/RecordQueryOrder.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.model.graphql +package com.lyneon.cytoidinfoquerier.data.constant object RecordQueryOrder { const val ASC = "ASC" diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/RecordQuerySort.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/RecordQuerySort.kt similarity index 79% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/RecordQuerySort.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/RecordQuerySort.kt index 7c620f5..23a3418 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/RecordQuerySort.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/constant/RecordQuerySort.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.model.graphql +package com.lyneon.cytoidinfoquerier.data.constant object RecordQuerySort { const val Score = "Score" diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/Analytics.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/graphql/Analytics.kt similarity index 56% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/Analytics.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/graphql/Analytics.kt index 777ac1c..787eb4c 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/Analytics.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/graphql/Analytics.kt @@ -1,8 +1,10 @@ -package com.lyneon.cytoidinfoquerier.model.graphql +package com.lyneon.cytoidinfoquerier.data.model.graphql +import com.lyneon.cytoidinfoquerier.data.constant.RecordQueryOrder +import com.lyneon.cytoidinfoquerier.data.constant.RecordQuerySort import com.lyneon.cytoidinfoquerier.logic.DateParser import com.lyneon.cytoidinfoquerier.logic.DateParser.formatToTimeString -import com.lyneon.cytoidinfoquerier.tool.extension.setPrecision +import com.lyneon.cytoidinfoquerier.util.extension.setPrecision import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import java.util.Locale @@ -13,7 +15,7 @@ data class Analytics( ) { @Serializable data class Data( - val profile: Profile + val profile: Profile? ) { @Serializable data class Profile( @@ -23,57 +25,56 @@ data class Analytics( } companion object { - fun getQueryBody( + fun getQueryString( cytoidID: String, recentRecordsLimit: Int = 0, recentRecordsSort: String = RecordQuerySort.Date, recentRecordsOrder: String = RecordQueryOrder.DESC, bestRecordsLimit: Int = 0 - ): String = GraphQL.getQueryString( - """{ - profile(uid:"$cytoidID"){ - recentRecords(limit:$recentRecordsLimit,sort:$recentRecordsSort,order:$recentRecordsOrder){ - ...UserRecord - }, - bestRecords(limit:$bestRecordsLimit){ - ...UserRecord - } + ) = """{ + profile(uid:"$cytoidID"){ + recentRecords(limit:$recentRecordsLimit,sort:$recentRecordsSort,order:$recentRecordsOrder){ + ...UserRecord + }, + bestRecords(limit:$bestRecordsLimit){ + ...UserRecord } } - - fragment UserRecord on UserRecord { - score, - accuracy, - mods, - details { - perfect, - great, - good, - bad, - miss, - maxCombo - }, - rating, - date, - chart { - difficulty, - type, - name, - notesCount, - level { - uid, - title, - bundle { - backgroundImage { - thumbnail, - original - } + } + + fragment UserRecord on UserRecord { + score + accuracy + mods + details { + perfect + great + good + bad + miss + maxCombo + } + rating + date + chart { + difficulty + type + name + notesCount + level { + uid + title + bundle { + backgroundImage { + thumbnail + original } + music + musicPreview } } } - """.replace("\n", "\\n").replace("\"", "\\\"") - ) + }""" fun decodeFromJSONString(json: String): Analytics { val jsonHandler = Json { ignoreUnknownKeys = true } @@ -90,18 +91,20 @@ data class UserRecord( val details: RecordDetails, val rating: Float, val date: String, - val chart: RecordChart + val chart: RecordChart? ) { fun detailsString(): String = StringBuilder().apply { val record = this@UserRecord - appendLine("${record.chart.level.title}(${ - record.chart.name - ?: record.chart.type.replaceFirstChar { - if (it.isLowerCase()) it.titlecase( - Locale.getDefault() - ) else it.toString() - } - } ${record.chart.difficulty})(${record.chart.level.uid})") + record.chart?.let { + appendLine("${record.chart.level?.title ?: "LevelTitle"}(${ + record.chart.name + ?: record.chart.type.replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.getDefault() + ) else it.toString() + } + } ${record.chart.difficulty})(${record.chart.level?.uid ?: "LevelUid"})") + } appendLine(record.score) appendLine("${(record.accuracy * 100).setPrecision(2)}% accuracy ${record.details.maxCombo} max combo") appendLine("Rating ${record.rating.setPrecision(2)}") @@ -125,7 +128,7 @@ data class UserRecord( val type: String, val name: String?, val notesCount: Int, - val level: RecordLevel + val level: RecordLevel? ) { @Serializable data class RecordLevel( @@ -135,7 +138,9 @@ data class UserRecord( ) { @Serializable data class LevelBundle( - val backgroundImage: Image + val backgroundImage: Image, + val music: String, + val musicPreview: String? = null ) { @Serializable data class Image( diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/graphql/ProfileGraphQL.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/graphql/ProfileGraphQL.kt new file mode 100644 index 0000000..e414303 --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/graphql/ProfileGraphQL.kt @@ -0,0 +1,217 @@ +package com.lyneon.cytoidinfoquerier.data.model.graphql + +import com.lyneon.cytoidinfoquerier.data.GraphQL +import com.lyneon.cytoidinfoquerier.json +import com.lyneon.cytoidinfoquerier.logic.network.NetRequest +import kotlinx.serialization.Serializable + +@Serializable +data class ProfileGraphQL( + val data: ProfileData +) { + @Serializable + data class ProfileData( + val profile: Profile + ) { + @Serializable + data class Profile( + val user: User, + val bio: String, + val badges: ArrayList, + val recentRecords: ArrayList + ) { + @Serializable + data class User( + val id: String, + val uid: String, + val registrationDate: String, + val lastSeen: String, + val avatar: Avatar, + val levelsCount: Int, + val levels: ArrayList, + val collectionsCount: Int, + val collections: ArrayList + ) { + @Serializable + data class Avatar( + val original: String, + val large: String + ) + + @Serializable + data class UserLevel( + val uid: String, + val title: String, + val description: String? = null, + val metadata: LevelMeta, + val bundle: LevelBundle, + val charts: ArrayList + ) { + @Serializable + data class LevelMeta( + val artist: ResourceMetaProperty + ) { + @Serializable + data class ResourceMetaProperty( + val name: String + ) + } + + @Serializable + data class LevelBundle( + val music: String, + val musicPreview: String? = null, + val backgroundImage: Image + ) { + @Serializable + data class Image( + val original: String, + val thumbnail: String + ) + } + + @Serializable + data class Chart( + val name: String?, + val type: String, + val difficulty: Int, + val notesCount: Int + ) + } + + @Serializable + data class CollectionUserListing( + val title: String, + val slogan: String, + val levelCount: Int, + val cover: Image + ) { + @Serializable + data class Image( + val original: String, + val thumbnail: String + ) + } + } + + @Serializable + data class Badge( + val title: String, + val description: String + ) + } + } + + companion object { + fun getQueryString(cytoidID: String) = """{ + profile(uid: "$cytoidID") { + user { + id + uid + registrationDate + lastSeen + avatar { + original + large + } + levelsCount + levels { + uid + title + metadata { + artist { + name + } + } + bundle { + music + musicPreview + backgroundImage { + original + thumbnail + } + } + charts { + name + type + difficulty + notesCount + } + } + collectionsCount + collections { + title + slogan + levelCount + cover { + original + thumbnail + } + } + } + bio + badges { + title + description + } + recentRecords(limit: 10) { + ...UserRecord + } + } + } + + fragment UserRecord on UserRecord { + score + accuracy + mods + details { + perfect + great + good + bad + miss + maxCombo + } + rating + date + chart { + difficulty + type + name + notesCount + level { + uid + title + bundle { + backgroundImage { + thumbnail + original + } + music + musicPreview + } + } + } + }""" + + fun get(cytoidID: String): ProfileGraphQL = + json.decodeFromString( + NetRequest.getGQLResponseJSONString( + GraphQL.getQueryString( + getQueryString(cytoidID) + ) + ) + ) + + fun getDefaultInstance(): ProfileGraphQL = ProfileGraphQL( + ProfileData( + ProfileData.Profile( + ProfileData.Profile.User( + "", "", "", "", ProfileData.Profile.User.Avatar("", ""), 0, + arrayListOf(), 0, arrayListOf() + ), "", arrayListOf(), arrayListOf() + ) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/webapi/Comment.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/webapi/Comment.kt new file mode 100644 index 0000000..9075b8d --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/webapi/Comment.kt @@ -0,0 +1,58 @@ +package com.lyneon.cytoidinfoquerier.data.model.webapi + +import com.lyneon.cytoidinfoquerier.json +import kotlinx.serialization.Serializable +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.IOException + +@Serializable +data class Comment( + val content: String, + val date: String, + val owner: Owner +) { + @Serializable + data class Owner( + val uid: String, + val avatar: Avatar + ) { + @Serializable + data class Avatar( + val original: String, + val small: String, + val medium: String, + val large: String + ) + } + + companion object { + fun get(id: String): ArrayList { + val response = try { + OkHttpClient().newCall( + Request.Builder() + .url("https://services.cytoid.io/threads/profile/$id") + .removeHeader("User-Agent").addHeader("User-Agent", "CytoidClient/2.1.1") + .build() + ).execute() + } catch (e: IOException) { + throw e + } + val result = try { + when (response.code) { + 200 -> response.body?.string() + else -> throw Exception("Unknown Exception: HTTP response code is${response.code}") + } + } finally { + response.body?.close() + } + return if (result == null) { + throw Exception("Response body is null!HTTP response code is ${response.code}") + } else { + json.decodeFromString(result) + } + } + } +} + + diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/webapi/Profile.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/webapi/ProfileWebapi.kt similarity index 58% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/model/webapi/Profile.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/webapi/ProfileWebapi.kt index 5af127b..048a793 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/webapi/Profile.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/data/model/webapi/ProfileWebapi.kt @@ -1,9 +1,13 @@ -package com.lyneon.cytoidinfoquerier.model.webapi +package com.lyneon.cytoidinfoquerier.data.model.webapi +import com.lyneon.cytoidinfoquerier.json import kotlinx.serialization.Serializable +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.IOException @Serializable -data class Profile( +data class ProfileWebapi( val user: User, val badges: ArrayList, val grade: Grade, @@ -22,7 +26,8 @@ data class Profile( ) { @Serializable data class Avatar( - val original: String + val original: String, + val large: String ) } @@ -105,4 +110,38 @@ data class Profile( val currentLevelExp: Int ) } + + companion object { + fun get(cytoidID: String): ProfileWebapi { + val response = try { + OkHttpClient().newCall( + Request.Builder() + .url("https://services.cytoid.io/profile/$cytoidID/details") + .removeHeader("User-Agent").addHeader("User-Agent", "CytoidClient/2.1.1") + .build() + ).execute() + } catch (e: IOException) { + throw e + } + val result = try { + when (response.code) { + 200 -> response.body?.string() + else -> throw Exception("Unknown Exception: HTTP response code is${response.code}") + } + } finally { + response.body?.close() + } + return if (result == null) { + throw Exception("Response body is null!HTTP response code is ${response.code}") + } else { + json.decodeFromString(result) + } + } + + fun getDefaultInstance(): ProfileWebapi = ProfileWebapi( + User("", User.Avatar("", "")), + arrayListOf(), Grade(), Activities(0, 0, 0, 0.0, 0, 0f), Exp(0, 0, 0, 0, 0, 0), 0.0, + arrayListOf(), null, null, Character("", null, Character.Exp(0, 0, 0, 0)) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/DateParser.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/DateParser.kt index 1d642af..ba37ed2 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/DateParser.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/DateParser.kt @@ -12,8 +12,13 @@ object DateParser { return dateFormat.parse(dateString) as Date } - fun Date.formatToTimeString(): String { - val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + /** + * 将当前Date对象转换为字符串表示 + * @param pattern 目标字符串的期望格式 + * @return Date对象的字符串表示 + */ + fun Date.formatToTimeString(pattern: String = "yyyy-MM-dd HH:mm:ss"): String { + val dateFormat = SimpleDateFormat(pattern, Locale.getDefault()) dateFormat.timeZone = TimeZone.getDefault() return dateFormat.format(this) } diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/ImageHandler.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/ImageHandler.kt index 0d35d6d..7acc92e 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/ImageHandler.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/ImageHandler.kt @@ -1,5 +1,3 @@ -@file:Suppress("NAME_SHADOWING") - package com.lyneon.cytoidinfoquerier.logic import android.graphics.Bitmap @@ -11,17 +9,29 @@ import android.graphics.Rect import android.graphics.RectF import android.graphics.Shader import android.text.TextPaint +import androidx.compose.ui.graphics.toArgb import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.scale import com.lyneon.cytoidinfoquerier.BaseApplication import com.lyneon.cytoidinfoquerier.R -import com.lyneon.cytoidinfoquerier.model.graphql.UserRecord -import com.lyneon.cytoidinfoquerier.model.webapi.Profile -import com.lyneon.cytoidinfoquerier.tool.extension.enableAntiAlias -import com.lyneon.cytoidinfoquerier.tool.extension.roundBitmap -import com.lyneon.cytoidinfoquerier.tool.extension.setPrecision -import com.lyneon.cytoidinfoquerier.tool.extension.toBitmap +import com.lyneon.cytoidinfoquerier.data.constant.CytoidColors +import com.lyneon.cytoidinfoquerier.data.constant.CytoidScoreRange +import com.lyneon.cytoidinfoquerier.data.constant.toIntArray +import com.lyneon.cytoidinfoquerier.data.model.graphql.UserRecord +import com.lyneon.cytoidinfoquerier.data.model.webapi.ProfileWebapi +import com.lyneon.cytoidinfoquerier.logic.DateParser.formatToTimeString +import com.lyneon.cytoidinfoquerier.util.ColumnBitmap +import com.lyneon.cytoidinfoquerier.util.RowBitmap +import com.lyneon.cytoidinfoquerier.util.extension.enableAntiAlias +import com.lyneon.cytoidinfoquerier.util.extension.isMaxCytoidGrade +import com.lyneon.cytoidinfoquerier.util.extension.roundBitmap +import com.lyneon.cytoidinfoquerier.util.extension.setPrecision +import com.lyneon.cytoidinfoquerier.util.extension.toBitmap +import com.patrykandpatrick.vico.core.extension.ceil +import com.patrykandpatrick.vico.core.extension.lineHeight +import com.patrykandpatrick.vico.core.extension.sumOf +import com.patrykandpatrick.vico.core.extension.textHeight import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.asCoroutineDispatcher @@ -32,11 +42,14 @@ import java.util.Date import java.util.Locale import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors +import kotlin.math.abs +import kotlin.math.roundToInt object ImageHandler { fun getRecordsImage( - profile: Profile, + profileWebapi: ProfileWebapi, records: List, + recordsType: String, columnsCount: Int = 5, keep2DecimalPlaces: Boolean = true ): Bitmap { @@ -74,7 +87,7 @@ object ImageHandler { // 绘制用户头像 val avatar = - URL(profile.user.avatar.original).toBitmap() + URL(profileWebapi.user.avatar.original).toBitmap() .scale(avatarDiameter, avatarDiameter, false).roundBitmap() canvas.drawBitmap(avatar, padding.toFloat(), padding.toFloat(), null) avatar.recycle() @@ -83,24 +96,29 @@ object ImageHandler { paint.textSize = 200f paint.color = Color.parseColor("#FFF8F8F2") canvas.drawText( - profile.user.uid, + profileWebapi.user.uid, (padding + avatarDiameter + padding).toFloat(), padding + 200f, paint ) paint.textSize = 75f canvas.drawText( - "Lv.${profile.exp.currentLevel} Rating ${ - if (keep2DecimalPlaces) profile.rating.setPrecision(2) - else profile.rating + "Lv.${profileWebapi.exp.currentLevel} Rating ${ + if (keep2DecimalPlaces) profileWebapi.rating.setPrecision(2) + else profileWebapi.rating }", (padding + avatarDiameter + padding).toFloat(), - padding + 200f + paint.fontMetrics.descent + 75f, + padding + 200f + paint.fontMetrics.descent + padding + 75f, + paint + ) + canvas.drawText( + "${records.size} $recordsType", + (padding + avatarDiameter + padding).toFloat(), + padding + 200f + paint.fontMetrics.descent + padding + 75f + padding + 75f, paint ) // 获取记录图像 - val recordImages = ArrayList(records.size) for (i in records.indices) { // 初始化记录图像列表 @@ -161,11 +179,7 @@ object ImageHandler { paint ) - recordImages.forEach { bitmap -> - if (!bitmap.isRecycled) { - bitmap.recycle() - } - } + recordImages.forEach { if (!it.isRecycled) it.recycle() } recordImages.clear() @@ -183,13 +197,14 @@ object ImageHandler { this.isAntiAlias = true } - paint.color = Color.parseColor("#7F000000") + paint.color = Color.parseColor("#80000000") paint.style = Paint.Style.FILL //绘制曲绘 canvas.drawBitmap( try { - URL(record.chart.level.bundle.backgroundImage.thumbnail).toBitmap() + if (record.chart?.level != null) URL(record.chart.level.bundle.backgroundImage.thumbnail).toBitmap() + else BaseApplication.context.getDrawable(R.drawable.sayakacry)!!.toBitmap() } catch (e: Exception) { BaseApplication.context.getDrawable(R.drawable.sayakacry)!!.toBitmap() }, @@ -200,53 +215,57 @@ object ImageHandler { // 绘制半透明灰色遮罩层 canvas.drawRect(0f, 0f, 576f, 360f, paint) - paint.textSize = 40f - val difficultyWidth = paint.measureText(" ${ - record.chart.name - ?: record.chart.type.replaceFirstChar { - if (it.isLowerCase()) it.titlecase( - Locale.getDefault() - ) else it.toString() - } - } ${record.chart.difficulty} ") - paint.shader = LinearGradient( - 10f, 20f, 10f + difficultyWidth, 70f, when (record.chart.type) { - "easy" -> intArrayOf(Color.parseColor("#4CA2CD"), Color.parseColor("#67B26F")) + record.chart?.let { chart -> + paint.textSize = 40f + val difficulty = " ${ + chart.name + ?: chart.type.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) + else it.toString() + } + } ${chart.difficulty} " + val difficultyWidth = paint.measureText(difficulty) + paint.shader = LinearGradient( + 10f, 20f, 10f + difficultyWidth, 70f, when (chart.type) { + "easy" -> intArrayOf(Color.parseColor("#4CA2CD"), Color.parseColor("#67B26F")) - "hard" -> intArrayOf(Color.parseColor("#B06ABC"), Color.parseColor("#4568DC")) + "hard" -> intArrayOf(Color.parseColor("#B06ABC"), Color.parseColor("#4568DC")) - "extreme" -> intArrayOf( - Color.parseColor("#6F0000"), - Color.parseColor("#200122") - ) + "extreme" -> intArrayOf( + Color.parseColor("#6F0000"), + Color.parseColor("#200122") + ) + + else -> intArrayOf(Color.parseColor("#B06ABC"), Color.parseColor("#4568DC")) + }, null, Shader.TileMode.CLAMP + ) + paint.alpha = 255 + canvas.drawRoundRect(RectF(10f, 20f, 10f + difficultyWidth, 70f), 50f, 50f, paint) + paint.color = Color.WHITE + paint.shader = null + canvas.drawText(difficulty, 10f, 60f, paint) + } - else -> intArrayOf(Color.parseColor("#B06ABC"), Color.parseColor("#4568DC")) - }, null, Shader.TileMode.CLAMP - ) - paint.alpha = 255 - canvas.drawRoundRect(RectF(10f, 20f, 10f + difficultyWidth, 70f), 50f, 50f, paint) - paint.color = Color.WHITE - paint.shader = null - canvas.drawText(" ${ - record.chart.name - ?: record.chart.type.replaceFirstChar { - if (it.isLowerCase()) it.titlecase( - Locale.getDefault() - ) else it.toString() - } - } ${record.chart.difficulty} ", 10f, 60f, paint) paint.textSize = 50f - canvas.drawText(record.chart.level.title, 10f, 130f, paint) - canvas.drawText( - "${record.score} ${ - if (keep2DecimalPlaces) (record.accuracy * 100).setPrecision(2) - else record.accuracy * 100 - }%", + record.chart?.level.let { canvas.drawText(it?.title ?: "LevelTitle", 10f, 130f, paint) } + + val score = "${record.score} ${ + if (keep2DecimalPlaces) (record.accuracy * 100).setPrecision(2) + else record.accuracy * 100 + }%" + val scoreWidth = paint.measureText(score) + paint.shader = if (record.score >= 995000) LinearGradient( 10f, 190f, - paint - ) + 10f + scoreWidth, + 240f, + if (record.score == 1000000) CytoidColors.maxColor.toIntArray() + else CytoidColors.sssColor.toIntArray(), + null, Shader.TileMode.CLAMP + ) else null + canvas.drawText(score, 10f, 190f, paint) + paint.shader = null paint.textSize = 30f canvas.drawText( "Rating ${ @@ -254,20 +273,374 @@ object ImageHandler { else record.rating }", 10f, 230f, paint ) + + canvas.drawText("Details: ", 10f, 270f, paint) + paint.color = Color.parseColor("#60a5fa") canvas.drawText( - "Details: ${record.details.perfect}/${record.details.great}/${record.details.good}/${record.details.bad}/${record.details.miss}", - 10f, + record.details.perfect.toString(), + 10f + paint.measureText("Details: "), 270f, paint ) + paint.color = Color.parseColor("#facc15") canvas.drawText( - "${record.details.maxCombo} combos ${if (record.score == 1000000) "AP" else if (record.details.maxCombo == record.chart.notesCount) "FC" else ""}", - 10f, - 310f, + record.details.great.toString(), + 10f + paint.measureText("Details: ${record.details.perfect}/"), + 270f, paint ) + paint.color = Color.parseColor("#4ade80") + canvas.drawText( + record.details.good.toString(), + 10f + paint.measureText("Details: ${record.details.perfect}/${record.details.great}/"), + 270f, + paint + ) + paint.color = Color.parseColor("#f87171") + canvas.drawText( + record.details.bad.toString(), + 10f + paint.measureText("Details: ${record.details.perfect}/${record.details.great}/${record.details.good}/"), + 270f, + paint + ) + paint.color = Color.parseColor("#94a3b8") + canvas.drawText( + record.details.miss.toString(), + 10f + paint.measureText("Details: ${record.details.perfect}/${record.details.great}/${record.details.good}/${record.details.bad}/"), + 270f, + paint + ) + paint.color = Color.parseColor("#ffffff") + record.chart?.let { + canvas.drawText( + "${record.details.maxCombo} combos ${if (record.score == 1000000) "All Perfect" else if (record.details.maxCombo == record.chart.notesCount) "Full Combo" else ""}", + 10f, + 310f, + paint + ) + } canvas.drawText("Mods:${record.mods}", 10f, 350f, paint) return bitmap } } + +object CytoidRecordsImageHandler2 { + val padding = 50 + val avatarDiameter = + listOf(160f, 70f, 50f).sumOf { Paint().apply { textSize = it }.lineHeight }.ceil.toInt() + val recordSpacing = 16 + val recordWidth = 576 + val recordHeight = 360 + val headerHeight = 300 + + fun getRecordsImage( + profileWebapi: ProfileWebapi, + records: List, + recordsType: String, + columnsCount: Int = 5, + keep2DecimalPlaces: Boolean = true + ): Bitmap { +// 行数 + val rowsCount = + if (records.size % columnsCount == 0) records.size / columnsCount else records.size / columnsCount + 1 +// 图片的总宽高 + val width = padding * 2 + recordWidth * columnsCount + recordSpacing * (columnsCount - 1) + val height = + padding * 2 + headerHeight + padding + recordHeight * rowsCount + recordSpacing * (rowsCount - 1) + 40 +// 初始化位图和绘制对象 + val bitmap = ColumnBitmap(padding = padding).apply { + setBackgroundColor( + 255, + (CytoidColors.backgroundColor.red * 255).roundToInt(), + (CytoidColors.backgroundColor.green * 255).roundToInt(), + (CytoidColors.backgroundColor.blue * 255).roundToInt() + ) + addBitmap(getHeaderImage(profileWebapi, keep2DecimalPlaces, records, recordsType)) + addSpace(32) + addBitmap(getRecordsGridImage(rowsCount, columnsCount, records, keep2DecimalPlaces)) + addSpace(32) + addText( + "${Date()} | Generated by ${BaseApplication.context.getString(R.string.app_name)}", + getDefaultPaint().apply { textSize = 32f } + ) + } + + return bitmap.getBitmap() + } + + private fun getHeaderImage( + profileWebapi: ProfileWebapi, + keep2DecimalPlaces: Boolean, + records: List, + recordsType: String + ): Bitmap = RowBitmap(contentSpacing = this@CytoidRecordsImageHandler2.padding).apply { + addBitmap( + URL(profileWebapi.user.avatar.original).toBitmap() + .scale(avatarDiameter, avatarDiameter, false).roundBitmap() + ) + addBitmap( + ColumnBitmap().apply { + addText(profileWebapi.user.uid, getDefaultPaint().apply { + textSize = 160f + }) + addText("Lv.${profileWebapi.exp.currentLevel} Rating ${ + profileWebapi.rating.run { if (keep2DecimalPlaces) this.setPrecision(2) else this } + }", getDefaultPaint().apply { + textSize = 70f + }) + addText("${records.size} $recordsType", getDefaultPaint().apply { textSize = 50f }) + }.getBitmap() + ) + }.getBitmap() + + private fun getRecordsGridImage( + rowsCount: Int, + columnsCount: Int, + records: List, + keep2DecimalPlaces: Boolean + ): Bitmap { + val bitmap = Bitmap.createBitmap( + columnsCount * recordWidth + (columnsCount - 1) * recordSpacing, + rowsCount * recordHeight + (rowsCount - 1) * recordSpacing, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap).apply { enableAntiAlias() } + val recordImages = getRecordImagesListFromRecordsList(records, keep2DecimalPlaces) + + var currentRecordImageIndex = 0 + canvas.drawBitmap(ColumnBitmap(contentSpacing = recordSpacing).apply { + for (row in 1..rowsCount) { + addBitmap(RowBitmap(contentSpacing = 16).apply { + for (column in 1..columnsCount) { + addBitmap(recordImages[currentRecordImageIndex]) + currentRecordImageIndex++ + if (currentRecordImageIndex == recordImages.size) break + } + }.getBitmap()) + } + }.getBitmap(), 0f, 0f, null) + + return bitmap + } + + private fun getRecordImagesListFromRecordsList( + records: List, + keep2DecimalPlaces: Boolean + ): List { + val recordImages = ArrayList(records.size) + for (i in records.indices) { +// 初始化记录图像列表 + recordImages.add(Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)) + } + val countDownLatch = CountDownLatch(records.size) + val dispatcher = Executors.newFixedThreadPool(32).asCoroutineDispatcher() + val job = Job() + for (i in records.indices) { + val record = records[i] + CoroutineScope(job + dispatcher).launch { + val recordImage = async { + getRecordImage(record, keep2DecimalPlaces) + }.await() + synchronized(ImageHandler::class.java) { + recordImages[i].recycle() + recordImages[i] = recordImage + } + countDownLatch.countDown() + } + } + countDownLatch.await() + job.cancel() + dispatcher.close() + + return recordImages + } + + private fun getRecordImage( + record: UserRecord, + keep2DecimalPlaces: Boolean + ): Bitmap { + val bitmap = Bitmap.createBitmap(576, 360, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap).apply { this.enableAntiAlias() } + + val contentPadding = 16 + val difficultySize = 24f + val chartTitleSize = 32f + val chartUIDSize = 16f + val scoreSize = 36f + val detailsSize = 24f + + //绘制曲绘 + canvas.drawBitmap( + try { + if (record.chart?.level != null) URL(record.chart.level.bundle.backgroundImage.thumbnail).toBitmap() + else BaseApplication.context.getDrawable(R.drawable.sayakacry)!!.toBitmap() + } catch (e: Exception) { + BaseApplication.context.getDrawable(R.drawable.sayakacry)!!.toBitmap() + }, + null, + Rect(0, 0, 576, 360), + null + ) +// 绘制半透明灰色遮罩层 + canvas.drawRect(0f, 0f, 576f, 360f, Paint().apply { + color = Color.parseColor("#80000000") + style = Paint.Style.FILL + }) + + canvas.drawBitmap(ColumnBitmap(padding = contentPadding).apply { + record.chart?.let { chart -> + val difficulty = " ${ + chart.name + ?: chart.type.replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.getDefault() + ) else it.toString() + } + } ${chart.difficulty} " + val score = "${record.score} ${ + (record.accuracy * 100).run { if (keep2DecimalPlaces) this.setPrecision(2) else this } + }%" + addBitmap(getDifficultyImage(difficulty, chart.type, difficultySize)) + addText( + chart.level?.title ?: "ChartTitle", + getDefaultPaint().apply { textSize = chartTitleSize } + ) + addText( + chart.level?.uid ?: "ChartUID", + getDefaultPaint().apply { textSize = chartUIDSize }) + addText(score, getDefaultPaint().apply { + textSize = scoreSize + if (record.score in CytoidScoreRange.sss) shader = + if (record.score.isMaxCytoidGrade()) LinearGradient( + 0f, + 0f, + this.measureText(score), + this.textHeight, + CytoidColors.maxColor.toIntArray(), + null, + Shader.TileMode.CLAMP + ) else LinearGradient( + 0f, + 0f, + this.measureText(score), + this.textHeight, + CytoidColors.sssColor.toIntArray(), + null, + Shader.TileMode.CLAMP + ) + } + ) + addText( + "${record.details.maxCombo}x " + + "${ + when (chart.notesCount) { + record.details.perfect -> "AP" + record.details.maxCombo -> "FC" + else -> "" + } + } | " + + "Rating ${ + record.rating.run { + if (keep2DecimalPlaces) this.setPrecision( + 2 + ) else this + } + }", + getDefaultPaint().apply { textSize = detailsSize } + ) + addBitmap(RowBitmap().apply { + addText("Details:", getDefaultPaint().apply { textSize = detailsSize }) + addText( + record.details.perfect.toString() + " ", + getDefaultPaint().apply { + textSize = detailsSize + color = CytoidColors.perfectColor.toArgb() + } + ) + addText( + record.details.great.toString() + " ", + getDefaultPaint().apply { + textSize = detailsSize + color = CytoidColors.greatColor.toArgb() + } + ) + addText( + record.details.good.toString() + " ", + getDefaultPaint().apply { + textSize = detailsSize + color = CytoidColors.goodColor.toArgb() + } + ) + addText( + record.details.bad.toString() + " ", + getDefaultPaint().apply { + textSize = detailsSize + color = CytoidColors.badColor.toArgb() + } + ) + addText( + record.details.miss.toString() + " ", + getDefaultPaint().apply { + textSize = detailsSize + color = CytoidColors.missColor.toArgb() + } + ) + }.getBitmap()) + addText("Mods:${record.mods}", getDefaultPaint().apply { textSize = detailsSize }) + addText( + DateParser.parseISO8601Date(record.date).formatToTimeString(), + getDefaultPaint().apply { textSize = detailsSize } + ) + } + }.getBitmap(), 0f, 0f, null) + + return bitmap + } + + private fun getDifficultyImage( + difficultyText: String, + difficultyType: String, + difficultySize: Float + ): Bitmap { + val paint = getDefaultPaint().apply { textSize = difficultySize } + val difficultyWidth = paint.measureText(difficultyText) + val difficultyHeight = paint.textHeight + val bitmap = Bitmap.createBitmap( + difficultyWidth.ceil.toInt() + 10, + difficultyHeight.ceil.toInt(), + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap).apply { enableAntiAlias() } + + canvas.drawRoundRect( + 0f, + 0f, + bitmap.width.toFloat(), + bitmap.height.toFloat(), + bitmap.height.toFloat() / 2, + bitmap.height.toFloat() / 2, + Paint().apply { + shader = LinearGradient( + 0f, 0f, difficultyWidth, difficultyHeight, when (difficultyType) { + "easy" -> CytoidColors.easyColor.toIntArray() + "extreme" -> CytoidColors.extremeColor.toIntArray() + else -> CytoidColors.hardColor.toIntArray() + }, null, Shader.TileMode.CLAMP + ) + } + ) + canvas.drawText(difficultyText, 5f, abs(paint.ascent()), paint) + + return bitmap + } + + private fun getDefaultPaint() = TextPaint().apply { + typeface = ResourcesCompat.getFont( + BaseApplication.context, + R.font.mplus_rounded_regular + ) + isAntiAlias = true + color = Color.WHITE + } +} diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/network/NetRequest.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/network/NetRequest.kt index d7f6387..72dfa55 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/network/NetRequest.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/network/NetRequest.kt @@ -1,23 +1,24 @@ package com.lyneon.cytoidinfoquerier.logic.network -import com.lyneon.cytoidinfoquerier.model.webapi.Profile -import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody - -val json = Json { ignoreUnknownKeys = true } +import java.io.IOException object NetRequest { fun getGQLResponseJSONString(GQLQueryString: String): String { - val response = OkHttpClient().newCall( - Request.Builder() - .url("https://services.cytoid.io/graphql") - .removeHeader("User-Agent").addHeader("User-Agent", "CytoidClient/2.1.1") - .post(GQLQueryString.toRequestBody("application/json".toMediaTypeOrNull())) - .build() - ).execute() + val response = try { + OkHttpClient().newCall( + Request.Builder() + .url("https://services.cytoid.io/graphql") + .removeHeader("User-Agent").addHeader("User-Agent", "CytoidClient/2.1.1") + .post(GQLQueryString.toRequestBody("application/json".toMediaTypeOrNull())) + .build() + ).execute() + } catch (e: IOException) { + throw e + } val result = try { when (response.code) { 200 -> response.body?.string() @@ -32,26 +33,4 @@ object NetRequest { return result } } - - fun getProfile(cytoidID: String): Profile { - val response = OkHttpClient().newCall( - Request.Builder() - .url("https://services.cytoid.io/profile/$cytoidID/details") - .removeHeader("User-Agent").addHeader("User-Agent", "CytoidClient/2.1.1") - .build() - ).execute() - val result = try { - when (response.code) { - 200 -> response.body?.string() - else -> throw Exception("Unknown Exception:HTTP response code ${response.code}.${response.body?.string()}") - } - } finally { - response.body?.close() - } - return if (result == null) { - throw Exception("Unknown Exception:response result is null!HTTP response code ${response.code}.${response.body?.string()}") - } else { - json.decodeFromString(result) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/service/ImageGenerateService.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/service/ImageGenerateService.kt index 4a08ef2..13917b2 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/service/ImageGenerateService.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/logic/service/ImageGenerateService.kt @@ -8,15 +8,15 @@ import android.os.Looper import androidx.core.app.NotificationCompat import com.lyneon.cytoidinfoquerier.BaseApplication import com.lyneon.cytoidinfoquerier.R -import com.lyneon.cytoidinfoquerier.logic.ImageHandler +import com.lyneon.cytoidinfoquerier.data.model.webapi.ProfileWebapi +import com.lyneon.cytoidinfoquerier.logic.CytoidRecordsImageHandler2 import com.lyneon.cytoidinfoquerier.logic.NotificationHandler import com.lyneon.cytoidinfoquerier.logic.NotificationHandler.registerNotificationChannel -import com.lyneon.cytoidinfoquerier.logic.network.NetRequest -import com.lyneon.cytoidinfoquerier.tool.extension.saveIntoMediaStore -import com.lyneon.cytoidinfoquerier.tool.extension.showToast import com.lyneon.cytoidinfoquerier.ui.compose.QueryType import com.lyneon.cytoidinfoquerier.ui.compose.response import com.lyneon.cytoidinfoquerier.ui.compose.responseIsInitialized +import com.lyneon.cytoidinfoquerier.util.extension.saveIntoMediaStore +import com.lyneon.cytoidinfoquerier.util.extension.showToast import kotlin.concurrent.thread class ImageGenerateService : Service() { @@ -27,21 +27,24 @@ class ImageGenerateService : Service() { cytoidID: String, columnsCount: Int, queryType: String, + queryCount: Int, keep2DecimalPlaces: Boolean ) = Intent(context, ImageGenerateService::class.java).apply { this.putExtra("cytoidID", cytoidID) this.putExtra("columnsCount", columnsCount) this.putExtra("queryType", queryType) + this.putExtra("queryCount", queryCount) this.putExtra("keep2DecimalPlaces", keep2DecimalPlaces) } } override fun onBind(intent: Intent?): IBinder? { - TODO("Not yet implemented") + //TODO("Not yet implemented") + return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (responseIsInitialized()) { + if (responseIsInitialized() && response.data.profile != null) { if (intent == null) throw Exception("intent cannot be null") val cytoidID = intent.getStringExtra("cytoidID") ?: throw Exception("cytoidID extra needed") @@ -50,6 +53,7 @@ class ImageGenerateService : Service() { intent.getStringExtra("queryType") ?: throw Exception("queryType extra needed") val keep2DecimalPlaces = intent.getBooleanExtra("keep2DecimalPlaces", true) + val queryCount = intent.getIntExtra("queryCount", 0) registerNotificationChannel( NotificationHandler.CHANNEL_ID_GENERATE_IMAGE, @@ -74,12 +78,14 @@ class ImageGenerateService : Service() { R.string.saving ).showToast() thread { - ImageHandler.getRecordsImage( - NetRequest.getProfile( - cytoidID - ), - if (queryType == QueryType.bestRecords) response.data.profile.bestRecords - else response.data.profile.recentRecords, + CytoidRecordsImageHandler2.getRecordsImage( + ProfileWebapi.get(cytoidID), + if (queryType == QueryType.bestRecords) response.data.profile!!.bestRecords.subList( + 0, + queryCount + ) + else response.data.profile!!.recentRecords.subList(0, queryCount), + queryType, columnsCount, keep2DecimalPlaces ).saveIntoMediaStore() diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/CytoidDeepLink.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/model/CytoidDeepLink.kt deleted file mode 100644 index 20b9ed2..0000000 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/CytoidDeepLink.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.lyneon.cytoidinfoquerier.model - -object CytoidDeepLink { - fun getDeepLink(levelUID: String): String = "cytoid://levels/$levelUID" -} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/GraphQL.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/GraphQL.kt deleted file mode 100644 index 332d5e0..0000000 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/model/graphql/GraphQL.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.lyneon.cytoidinfoquerier.model.graphql - -object GraphQL { - fun getQueryString(query: String): String = """ - { - "operationName":null, - "variables":{}, - "query":"$query" - } - """ -} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/activity/MainActivity.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/activity/MainActivity.kt index 033df86..ce0fc57 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/activity/MainActivity.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/activity/MainActivity.kt @@ -6,14 +6,15 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ExitToApp +import androidx.compose.material.icons.automirrored.filled.ShowChart import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.ExitToApp import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button @@ -26,27 +27,31 @@ import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.lyneon.cytoidinfoquerier.BaseActivity import com.lyneon.cytoidinfoquerier.BaseApplication +import com.lyneon.cytoidinfoquerier.BaseApplication.Companion.globalDrawerState import com.lyneon.cytoidinfoquerier.R -import com.lyneon.cytoidinfoquerier.SecretData +import com.lyneon.cytoidinfoquerier.Secret +import com.lyneon.cytoidinfoquerier.data.constant.MMKVKeys import com.lyneon.cytoidinfoquerier.ui.compose.AnalyticsCompose import com.lyneon.cytoidinfoquerier.ui.compose.HomeCompose -import com.lyneon.cytoidinfoquerier.ui.compose.NavRoute +import com.lyneon.cytoidinfoquerier.data.constant.NavRoute +import com.lyneon.cytoidinfoquerier.ui.compose.GridColumnsSettingCompose import com.lyneon.cytoidinfoquerier.ui.compose.ProfileCompose import com.lyneon.cytoidinfoquerier.ui.compose.SettingsCompose -import com.lyneon.cytoidinfoquerier.ui.compose.SettingsMMKVKeys import com.lyneon.cytoidinfoquerier.ui.theme.CytoidInfoQuerierComposeTheme import com.microsoft.appcenter.AppCenter import com.microsoft.appcenter.analytics.Analytics @@ -60,9 +65,9 @@ class MainActivity : BaseActivity() { val mmkv = MMKV.defaultMMKV() - if (mmkv.decodeBool(SettingsMMKVKeys.enableAppCenter, true)) { + if (mmkv.decodeBool(MMKVKeys.ENABLE_APP_CENTER, true)) { AppCenter.start( - BaseApplication.context, SecretData.microsoftAppCenterAppSecret, + BaseApplication.context, Secret.microsoftAppCenterAppSecret, Analytics::class.java, Crashes::class.java ) } @@ -74,90 +79,120 @@ class MainActivity : BaseActivity() { color = MaterialTheme.colorScheme.background ) { val navController = rememberNavController() - BaseApplication.globalDrawerState = + var currentNavRoute by remember { mutableStateOf(NavRoute.home) } + globalDrawerState = rememberDrawerState(initialValue = DrawerValue.Closed) ModalNavigationDrawer( - drawerState = BaseApplication.globalDrawerState, + drawerState = globalDrawerState, drawerContent = { ModalDrawerSheet { - Column { + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + val scope = rememberCoroutineScope() Image( - painter = painterResource(id = R.drawable.tutorial_background), - contentDescription = stringResource(id = R.string.drawer_background) + painter = painterResource(R.drawable.tutorial_background), + contentDescription = stringResource(id = R.string.drawerMenu) ) - Spacer(modifier = Modifier.height(6.dp)) - Column { - Column( - modifier = Modifier.weight(1f) - ) { - val drawerItems = listOf( - DrawerItem( - icon = Icons.Filled.Home, - label = stringResource(id = R.string.home), - navDestinationRoute = NavRoute.home - ), - DrawerItem( - icon = ImageVector.vectorResource(id = R.drawable.baseline_insights_24), - label = stringResource(id = R.string.analytics), - navDestinationRoute = NavRoute.analytics - ), - DrawerItem( - icon = Icons.Filled.AccountCircle, - label = stringResource(id = R.string.profile), - navDestinationRoute = NavRoute.profile + Column( + Modifier.padding(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + NavigationDrawerItem( + label = { + Text(text = stringResource(R.string.home)) + }, + icon = { + Icon( + Icons.Filled.Home, + stringResource(R.string.home) ) - ) - val scope = rememberCoroutineScope() - drawerItems.forEach { - NavigationDrawerItem( - icon = { Icon(it.icon, it.label) }, - label = { Text(text = it.label) }, - selected = false, - onClick = { - navController.navigate(it.navDestinationRoute) - scope.launch { - BaseApplication.globalDrawerState.close() - } - }, - modifier = Modifier.padding(6.dp) - ) - } - } - Row( - horizontalArrangement = Arrangement.spacedBy( - 6.dp, - Alignment.End - ), - modifier = Modifier - .padding(6.dp) - .fillMaxWidth(), - ) { - val scope = rememberCoroutineScope() - Button(onClick = { - navController.navigate(NavRoute.settings) + }, + selected = currentNavRoute == NavRoute.home, + onClick = { + navController.navigate(NavRoute.home) + currentNavRoute = NavRoute.home scope.launch { - BaseApplication.globalDrawerState.close() + globalDrawerState.close() } - }) { - Column { - Icon( - imageVector = Icons.Filled.Settings, - contentDescription = stringResource(id = R.string.settings), - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - Text(text = stringResource(id = R.string.settings)) + } + ) + NavigationDrawerItem( + label = { + Text(text = stringResource(R.string.analytics)) + }, + icon = { + Icon( + Icons.AutoMirrored.Filled.ShowChart, + stringResource(R.string.analytics) + ) + }, + selected = currentNavRoute == NavRoute.analytics, + onClick = { + navController.navigate(NavRoute.analytics) + currentNavRoute = NavRoute.analytics + scope.launch { + globalDrawerState.close() } } - Button(onClick = { this@MainActivity.finish() }) { - Column { - Icon( - imageVector = Icons.Filled.ExitToApp, - contentDescription = stringResource(id = R.string.exit), - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - Text(text = stringResource(id = R.string.exit)) + ) + NavigationDrawerItem( + label = { + Text(text = stringResource(R.string.profile)) + }, + icon = { + Icon( + Icons.Default.AccountCircle, + stringResource(R.string.profile) + ) + }, + selected = currentNavRoute == NavRoute.profile, + onClick = { + navController.navigate(NavRoute.profile) + currentNavRoute = NavRoute.profile + scope.launch { + globalDrawerState.close() } } + ) + } + } + Row( + horizontalArrangement = Arrangement.spacedBy( + 6.dp, + Alignment.End + ), + modifier = Modifier + .padding(6.dp) + .fillMaxWidth(), + ) { + val scope = rememberCoroutineScope() + Button(onClick = { + navController.navigate(NavRoute.settings) + currentNavRoute = NavRoute.settings + scope.launch { + globalDrawerState.close() + } + }) { + Column { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = stringResource(id = R.string.settings), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Text(text = stringResource(id = R.string.settings)) + } + } + Button(onClick = { this@MainActivity.finish() }) { + Column { + Icon( + imageVector = Icons.AutoMirrored.Filled.ExitToApp, + contentDescription = stringResource(id = R.string.exit), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Text(text = stringResource(id = R.string.exit)) } } } @@ -179,7 +214,10 @@ class MainActivity : BaseActivity() { ProfileCompose() } composable(NavRoute.settings) { - SettingsCompose() + SettingsCompose(navController) + } + composable(NavRoute.gridColumnsSetting) { + GridColumnsSettingCompose(navController) } } } @@ -188,10 +226,4 @@ class MainActivity : BaseActivity() { } } } -} - -data class DrawerItem( - val icon: ImageVector, - val label: String, - val navDestinationRoute: String -) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/AnalyticsCompose.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/AnalyticsCompose.kt index 99f9a57..7166b3e 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/AnalyticsCompose.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/AnalyticsCompose.kt @@ -2,24 +2,23 @@ package com.lyneon.cytoidinfoquerier.ui.compose import android.Manifest import android.content.pm.PackageManager +import android.content.res.Configuration import android.os.Build import android.os.Looper -import android.widget.Toast -import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu @@ -44,23 +43,23 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.checkSelfPermission import com.lyneon.cytoidinfoquerier.R +import com.lyneon.cytoidinfoquerier.data.GraphQL +import com.lyneon.cytoidinfoquerier.data.constant.MMKVKeys +import com.lyneon.cytoidinfoquerier.data.model.graphql.Analytics import com.lyneon.cytoidinfoquerier.logic.network.NetRequest import com.lyneon.cytoidinfoquerier.logic.service.ImageGenerateService -import com.lyneon.cytoidinfoquerier.model.graphql.Analytics -import com.lyneon.cytoidinfoquerier.tool.extension.isValidCytoidID -import com.lyneon.cytoidinfoquerier.tool.extension.saveIntoClipboard -import com.lyneon.cytoidinfoquerier.tool.extension.showDialog -import com.lyneon.cytoidinfoquerier.tool.extension.showToast import com.lyneon.cytoidinfoquerier.ui.activity.MainActivity +import com.lyneon.cytoidinfoquerier.ui.compose.component.AlertCard import com.lyneon.cytoidinfoquerier.ui.compose.component.RecordCard import com.lyneon.cytoidinfoquerier.ui.compose.component.TopBar +import com.lyneon.cytoidinfoquerier.util.extension.isValidCytoidID +import com.lyneon.cytoidinfoquerier.util.extension.showToast import com.microsoft.appcenter.crashes.Crashes import com.tencent.mmkv.MMKV import kotlin.concurrent.thread lateinit var response: Analytics -@RequiresApi(Build.VERSION_CODES.O) @Composable fun AnalyticsCompose() { val context = LocalContext.current as MainActivity @@ -75,369 +74,408 @@ fun AnalyticsCompose() { var queryCountIsNull by remember { mutableStateOf(false) } var columnsCountIsNull by remember { mutableStateOf(false) } var querySettingsMenuIsExpanded by remember { mutableStateOf(false) } + var hideInput by remember { mutableStateOf(false) } + var error by remember { mutableStateOf("") } Column { - TopBar(title = stringResource(id = R.string.analytics)) + TopBar( + title = stringResource(id = R.string.analytics), + additionalActions = { + if (hideInput) IconButton(onClick = { hideInput = false }) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = stringResource(id = R.string.unfold) + ) + } + } + ) Column(modifier = Modifier.padding(6.dp, 6.dp, 6.dp)) { - Column { - var textFieldIsError by remember { mutableStateOf(false) } - var textFieldIsEmpty by remember { mutableStateOf(false) } - TextField( - isError = textFieldIsError or textFieldIsEmpty, - value = cytoidID, - onValueChange = { - cytoidID = it - textFieldIsError = !it.isValidCytoidID() - textFieldIsEmpty = it.isEmpty() - }, - label = { Text(text = stringResource(id = R.string.playerName)) }, - modifier = Modifier.fillMaxWidth(), - trailingIcon = { - Row( - Modifier.padding(horizontal = 6.dp) - ) { - IconButton(onClick = { querySettingsMenuIsExpanded = true }) { - Icon( - imageVector = Icons.Filled.Settings, - contentDescription = stringResource(id = R.string.querySettings) - ) - DropdownMenu( - expanded = querySettingsMenuIsExpanded, - onDismissRequest = { - querySettingsMenuIsExpanded = false - }) { - DropdownMenuItem( - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.best_records)) - RadioButton( - selected = queryType == QueryType.bestRecords, - onClick = { - queryType = QueryType.bestRecords - } - ) - } - }, - onClick = { queryType = QueryType.bestRecords } + AnimatedVisibility(visible = !hideInput) { + Column { + var textFieldIsError by remember { mutableStateOf(false) } + var textFieldIsEmpty by remember { mutableStateOf(false) } + TextField( + isError = textFieldIsError or textFieldIsEmpty, + value = cytoidID, + onValueChange = { + cytoidID = it + textFieldIsError = !it.isValidCytoidID() + textFieldIsEmpty = it.isEmpty() + }, + label = { Text(text = stringResource(id = R.string.playerName)) }, + modifier = Modifier.fillMaxWidth(), + trailingIcon = { + Row( + Modifier.padding(horizontal = 6.dp) + ) { + IconButton(onClick = { hideInput = true }) { + Icon( + imageVector = Icons.Default.KeyboardArrowUp, + contentDescription = stringResource(id = R.string.fold) ) - DropdownMenuItem( - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.recent_records)) - RadioButton( - selected = queryType == QueryType.recentRecords, - onClick = { - queryType = QueryType.recentRecords - } - ) - } - }, - onClick = { queryType = QueryType.recentRecords } + } + IconButton(onClick = { querySettingsMenuIsExpanded = true }) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = stringResource(id = R.string.querySettings) ) - DropdownMenuItem( - text = { - Column( - modifier = Modifier.fillMaxWidth() - ) { - TextField( - value = queryCount, - onValueChange = { - queryCountIsNull = it.isEmpty() - queryCount = it - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number - ), - singleLine = true, - isError = queryCountIsNull, - label = { Text(text = stringResource(id = R.string.query_count)) } - ) - AnimatedVisibility(visible = queryCountIsNull) { - Text( - text = stringResource(id = R.string.empty_queryCount), - color = Color.Red + DropdownMenu( + expanded = querySettingsMenuIsExpanded, + onDismissRequest = { + querySettingsMenuIsExpanded = false + } + ) { + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.best_records)) + RadioButton( + selected = queryType == QueryType.bestRecords, + onClick = { + queryType = QueryType.bestRecords + } ) } - } - }, - onClick = {} - ) - DropdownMenuItem( - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.ignore_cache)) - Checkbox( - checked = ignoreCache, - onCheckedChange = { - ignoreCache = it - } - ) - } - }, - onClick = { ignoreCache = !ignoreCache } - ) - DropdownMenuItem( - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.keep_2_decimal_places)) - Checkbox( - checked = keep2DecimalPlace, - onCheckedChange = { - keep2DecimalPlace = it + }, + onClick = { queryType = QueryType.bestRecords } + ) + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.recent_records)) + RadioButton( + selected = queryType == QueryType.recentRecords, + onClick = { + queryType = QueryType.recentRecords + } + ) + } + }, + onClick = { queryType = QueryType.recentRecords } + ) + DropdownMenuItem( + text = { + Column( + modifier = Modifier.fillMaxWidth() + ) { + TextField( + value = queryCount, + onValueChange = { + queryCountIsNull = it.isEmpty() + queryCount = it + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ), + singleLine = true, + isError = queryCountIsNull, + label = { Text(text = stringResource(id = R.string.query_count)) } + ) + AnimatedVisibility(visible = queryCountIsNull) { + Text( + text = stringResource(id = R.string.empty_queryCount), + color = Color.Red + ) } - ) - } - }, - onClick = { keep2DecimalPlace = !keep2DecimalPlace } - ) - DropdownMenuItem( - text = { - Column( - modifier = Modifier.fillMaxWidth() - ) { - TextField( - value = columnsCount, - onValueChange = { - columnsCountIsNull = it.isEmpty() - columnsCount = it - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number - ), - singleLine = true, - isError = columnsCountIsNull, - label = { Text(text = stringResource(id = R.string.columns_count)) }, - trailingIcon = { - TextButton(onClick = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - context.requestPermissions( - arrayOf( + } + }, + onClick = {} + ) + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.ignore_cache)) + Checkbox( + checked = ignoreCache, + onCheckedChange = { + ignoreCache = it + } + ) + } + }, + onClick = { ignoreCache = !ignoreCache } + ) + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.keep_2_decimal_places)) + Checkbox( + checked = keep2DecimalPlace, + onCheckedChange = { + keep2DecimalPlace = it + } + ) + } + }, + onClick = { keep2DecimalPlace = !keep2DecimalPlace } + ) + DropdownMenuItem( + text = { + Column( + modifier = Modifier.fillMaxWidth() + ) { + TextField( + value = columnsCount, + onValueChange = { + columnsCountIsNull = it.isEmpty() + columnsCount = it + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ), + singleLine = true, + isError = columnsCountIsNull, + label = { Text(text = stringResource(id = R.string.columns_count)) }, + trailingIcon = { + TextButton(onClick = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checkSelfPermission( + context, Manifest.permission.POST_NOTIFICATIONS - ), 1 - ) + ) != PackageManager.PERMISSION_GRANTED + ) { + context.requestPermissions( + arrayOf( + Manifest.permission.POST_NOTIFICATIONS + ), 1 + ) + } + val intent = + ImageGenerateService.getStartIntent( + context, + cytoidID, + columnsCount.toInt(), + queryType, + queryCount.toInt(), + keep2DecimalPlace + ) + context.startService(intent) + }) { + Text(text = stringResource(id = R.string.save_as_picture)) } - val intent = - ImageGenerateService.getStartIntent( - context, - cytoidID, - columnsCount.toInt(), - queryType, - keep2DecimalPlace - ) - context.startService(intent) - }) { - Text(text = stringResource(id = R.string.save_as_picture)) } - } - ) - AnimatedVisibility(visible = columnsCountIsNull) { - Text( - text = stringResource(id = R.string.empty_columnsCount), - color = Color.Red ) - } - } - }, - onClick = {} - ) - } - } - TextButton( - onClick = { - if (cytoidID.isEmpty()) { - context.getString(R.string.empty_cytoidID) - .showToast() - textFieldIsEmpty = true - } else if (!cytoidID.isValidCytoidID()) { - context.getString(R.string.invalid_cytoidID) - .showToast() - textFieldIsError = true - } else if (queryCountIsNull) { - context.getString(R.string.empty_queryCount) - .showToast() - querySettingsMenuIsExpanded = true - } else { - textFieldIsError = false - isQueryingFinished = false - if (System.currentTimeMillis() - mmkv.decodeLong( - "lastQueryProfileTime_${cytoidID}_${queryType}", - -1 - ) <= (6 * 60 * 60 * 1000) && !ignoreCache - ) { - response = try { - var toIndex: Int - val analytics = Analytics.decodeFromJSONString( - mmkv.decodeString("profileString_${cytoidID}_${queryType}") - ?: throw Exception() - ).apply { - if (queryType == QueryType.bestRecords) { - toIndex = - if (queryCount.toInt() <= this.data.profile.bestRecords.size) queryCount.toInt() - else this.data.profile.bestRecords.size - this.data.profile.bestRecords = - ArrayList( - this.data.profile.bestRecords.subList( - 0, - toIndex - ) - ) - } else { - toIndex = - if (queryCount.toInt() <= this.data.profile.recentRecords.size) queryCount.toInt() - else this.data.profile.recentRecords.size - this.data.profile.recentRecords = - ArrayList( - this.data.profile.recentRecords.subList( - 0, - toIndex - ) - ) + AnimatedVisibility(visible = columnsCountIsNull) { + Text( + text = stringResource(id = R.string.empty_columnsCount), + color = Color.Red + ) } } - "6小时内有查询记录,使用已缓存的数据,共${toIndex}条数据".showToast() - analytics - } catch (e: Exception) { - e.stackTraceToString().showDialog( - context, - context.getString(R.string.fail) - ) - Crashes.trackError(e) - return@TextButton - } - isQueryingFinished = true + }, + onClick = {} + ) + } + } + TextButton( + onClick = { + error = "" + if (cytoidID.isEmpty()) { + context.getString(R.string.empty_cytoidID) + .showToast() + textFieldIsEmpty = true + } else if (!cytoidID.isValidCytoidID()) { + context.getString(R.string.invalid_cytoidID) + .showToast() + textFieldIsError = true + } else if (queryCountIsNull) { + context.getString(R.string.empty_queryCount) + .showToast() + querySettingsMenuIsExpanded = true } else { - "开始查询$cytoidID".showToast() - thread { - try { - if (queryType == QueryType.bestRecords) { - Analytics.getQueryBody( - cytoidID = cytoidID, - bestRecordsLimit = queryCount.toInt(), - recentRecordsLimit = queryCount.toInt() - ) - } else { - Analytics.getQueryBody( - cytoidID = cytoidID, - bestRecordsLimit = queryCount.toInt(), - recentRecordsLimit = queryCount.toInt() - ) - }.saveIntoClipboard() - + textFieldIsError = false + isQueryingFinished = false + if (System.currentTimeMillis() - mmkv.decodeLong( + "lastQueryAnalyticsTime_${cytoidID}_${queryType}", + -1 + ) <= (6 * 60 * 60 * 1000) && !ignoreCache + ) { + response = try { + var toIndex: Int val profileString = - NetRequest.getGQLResponseJSONString( - if (queryType == QueryType.bestRecords) { - Analytics.getQueryBody( - cytoidID = cytoidID, - bestRecordsLimit = queryCount.toInt(), - recentRecordsLimit = queryCount.toInt() - ) - } else { - Analytics.getQueryBody( - cytoidID = cytoidID, - bestRecordsLimit = queryCount.toInt(), - recentRecordsLimit = queryCount.toInt() - ) + mmkv.decodeString("analyticsString_${cytoidID}_${queryType}") + if (profileString == null) { + error = "Failed to find cache data" + return@TextButton + } + val analytics = + Analytics.decodeFromJSONString(profileString) + .apply { + if (this.data.profile != null) { + val profile = this.data.profile + if (queryType == QueryType.bestRecords) { + toIndex = + if (queryCount.toInt() <= profile.bestRecords.size) queryCount.toInt() + else profile.bestRecords.size + profile.bestRecords = + ArrayList( + profile.bestRecords.subList( + 0, + toIndex + ) + ) + } else { + toIndex = + if (queryCount.toInt() <= profile.recentRecords.size) queryCount.toInt() + else profile.recentRecords.size + profile.recentRecords = + ArrayList( + profile.recentRecords.subList( + 0, + toIndex + ) + ) + } + } else { + error = + "local cache data.profile is null!" + return@TextButton + } } - ) - response = - Analytics.decodeFromJSONString( - profileString - ) - mmkv.encode( - "lastQueryProfileTime_${cytoidID}_${queryType}", - System.currentTimeMillis() - ) - mmkv.encode( - "profileString_${cytoidID}_${queryType}", - profileString - ) - Looper.prepare() - "查询${cytoidID}完成,共查询到${ - if (queryType == QueryType.bestRecords) response.data.profile.bestRecords.size - else response.data.profile.recentRecords.size - }条数据".showToast() - isQueryingFinished = true + "6小时内有查询记录,使用已缓存的数据,共${toIndex}条数据".showToast() + analytics } catch (e: Exception) { - Looper.prepare() - "查询失败:${e.stackTraceToString()}".showToast( - Toast.LENGTH_LONG - ) - e.printStackTrace() + error = e.stackTraceToString() Crashes.trackError(e) + return@TextButton + } + isQueryingFinished = true + } else { + "开始查询$cytoidID".showToast() + thread { + try { + val profileString = + NetRequest.getGQLResponseJSONString( + GraphQL.getQueryString( + if (queryType == QueryType.bestRecords) { + Analytics.getQueryString( + cytoidID = cytoidID, + bestRecordsLimit = queryCount.toInt(), + recentRecordsLimit = queryCount.toInt() + ) + } else { + Analytics.getQueryString( + cytoidID = cytoidID, + bestRecordsLimit = queryCount.toInt(), + recentRecordsLimit = queryCount.toInt() + ) + } + ) + ) + response = + Analytics.decodeFromJSONString( + profileString + ) + if (response.data.profile == null) { + error = "data.profile is null!" + return@thread + } else { + mmkv.encode( + "lastQueryAnalyticsTime_${cytoidID}_${queryType}", + System.currentTimeMillis() + ) + mmkv.encode( + "analyticsString_${cytoidID}_${queryType}", + profileString + ) + Looper.prepare() + "查询${cytoidID}完成,共查询到${ + if (queryType == QueryType.bestRecords) response.data.profile!!.bestRecords.size + else response.data.profile!!.recentRecords.size + }条数据".showToast() + isQueryingFinished = true + } + } catch (e: Exception) { + error = "查询失败:${e.stackTraceToString()}" + Crashes.trackError(e) + } } } } } + ) { + Text(text = stringResource(id = R.string.query)) } - ) { - Text(text = stringResource(id = R.string.query)) } - } - }, - singleLine = true - ) - AnimatedVisibility(visible = textFieldIsError) { - Text( - text = stringResource(id = R.string.invalid_cytoidID), - color = Color.Red - ) - } - AnimatedVisibility(visible = textFieldIsEmpty) { - Text( - text = stringResource(id = R.string.empty_cytoidID), - color = Color.Red + }, + singleLine = true ) + AnimatedVisibility(visible = textFieldIsError) { + Text( + text = stringResource(id = R.string.invalid_cytoidID), + color = Color.Red + ) + } + AnimatedVisibility(visible = textFieldIsEmpty) { + Text( + text = stringResource(id = R.string.empty_cytoidID), + color = Color.Red + ) + } } } - LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(320.dp), - contentPadding = PaddingValues(top = 6.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - if (isQueryingFinished && ::response.isInitialized) { - var remainRecord = if (queryCount.isEmpty()) 0 else queryCount.toInt() - for (i in 0 until - if (queryType == QueryType.bestRecords) response.data.profile.bestRecords.size - else response.data.profile.recentRecords.size - ) { - if (remainRecord == 0) break - val record = - if (queryType == QueryType.bestRecords) response.data.profile.bestRecords[i] - else response.data.profile.recentRecords[i] - item( - span = if ((if (queryType == QueryType.bestRecords) response.data.profile.bestRecords.size - else response.data.profile.recentRecords.size) == 1 - ) StaggeredGridItemSpan.FullLine - else StaggeredGridItemSpan.SingleLane - ) { - RecordCard( - record = record, - recordIndex = i + 1, - keep2DecimalPlace + if (error.isNotEmpty()) { + AlertCard(message = error, modifier = Modifier.padding(vertical = 6.dp)) + } else { + AnimatedVisibility(visible = isQueryingFinished && ::response.isInitialized) { + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Fixed( + mmkv.decodeInt( + if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) MMKVKeys.GRID_COLUMNS_COUNT_PORTRAIT + else MMKVKeys.GRID_COLUMNS_COUNT_LANDSCAPE, 1 ) - Spacer(modifier = Modifier.height(6.dp)) + ), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalItemSpacing = 6.dp, + contentPadding = PaddingValues(vertical = 6.dp) + ) { + if (isQueryingFinished && ::response.isInitialized) { + if (response.data.profile != null) { + var remainRecord = + if (queryCount.isEmpty()) 0 else queryCount.toInt() + for (i in 0 until + if (queryType == QueryType.bestRecords) response.data.profile!!.bestRecords.size + else response.data.profile!!.recentRecords.size + ) { + if (remainRecord == 0) break + val record = + if (queryType == QueryType.bestRecords) response.data.profile!!.bestRecords[i] + else response.data.profile!!.recentRecords[i] + item( + span = if ((if (queryType == QueryType.bestRecords) response.data.profile!!.bestRecords.size + else response.data.profile!!.recentRecords.size) == 1 + ) StaggeredGridItemSpan.FullLine + else StaggeredGridItemSpan.SingleLane + ) { + RecordCard( + record = record, + recordIndex = i + 1, + keep2DecimalPlace + ) + } + remainRecord-- + } + } else { + item { + AlertCard(message = "data.profile is null!") + } + } } - remainRecord-- } } } diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/CrashActivityCompose.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/CrashActivityCompose.kt index df4bf3d..e9f6aba 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/CrashActivityCompose.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/CrashActivityCompose.kt @@ -9,6 +9,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -16,13 +19,11 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.lyneon.cytoidinfoquerier.R -import com.lyneon.cytoidinfoquerier.tool.extension.saveIntoClipboard +import com.lyneon.cytoidinfoquerier.util.extension.saveIntoClipboard import com.lyneon.cytoidinfoquerier.ui.activity.CrashActivity @@ -45,7 +46,7 @@ fun CrashActivityCompose(crashMessage: String) { Process.killProcess(Process.myPid()) }) { Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_menu_restart), + imageVector = Icons.Default.Refresh, contentDescription = stringResource(id = R.string.restart) ) Spacer(modifier = Modifier.width(6.dp)) @@ -62,7 +63,7 @@ fun CrashActivityCompose(crashMessage: String) { Text(text = stringResource(id = R.string.copy)) Spacer(modifier = Modifier.width(6.dp)) Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_menu_copy), + imageVector = Icons.Default.ContentCopy, contentDescription = "复制" ) } diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/GridColumnsSettingCompose.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/GridColumnsSettingCompose.kt new file mode 100644 index 0000000..ce2795a --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/GridColumnsSettingCompose.kt @@ -0,0 +1,191 @@ +package com.lyneon.cytoidinfoquerier.ui.compose + +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.icons.filled.ScreenRotation +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.lyneon.cytoidinfoquerier.R +import com.lyneon.cytoidinfoquerier.data.constant.MMKVKeys +import com.lyneon.cytoidinfoquerier.data.constant.NavRoute +import com.lyneon.cytoidinfoquerier.ui.activity.MainActivity +import com.tencent.mmkv.MMKV + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GridColumnsSettingCompose(navController: NavController) { + val context = LocalContext.current as MainActivity + val orientation by remember { mutableIntStateOf(context.resources.configuration.orientation) } + val mmkv = MMKV.defaultMMKV() + var columnsCount by remember { mutableIntStateOf(1) } + + columnsCount = mmkv.decodeInt( + if (orientation == Configuration.ORIENTATION_PORTRAIT) + MMKVKeys.GRID_COLUMNS_COUNT_PORTRAIT + else MMKVKeys.GRID_COLUMNS_COUNT_LANDSCAPE, 1 + ) + if (columnsCount < 1) { + columnsCount = 1 + mmkv.encode( + if (orientation == Configuration.ORIENTATION_PORTRAIT) + MMKVKeys.GRID_COLUMNS_COUNT_PORTRAIT + else MMKVKeys.GRID_COLUMNS_COUNT_LANDSCAPE, 1 + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = stringResource( + id = if (orientation == Configuration.ORIENTATION_PORTRAIT) + R.string.grid_cloumns_count_portrait + else R.string.grid_cloumns_count_landscape + ) + "当前:$columnsCount" + ) + }, + navigationIcon = { + IconButton(onClick = { navController.navigate(NavRoute.settings) }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.back) + ) + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp), + horizontalArrangement = Arrangement.Center + ) { + AnimatedVisibility(visible = columnsCount != 1) { + Button( + onClick = { + if (columnsCount != 1) { + columnsCount-- + mmkv.encode( + if (orientation == Configuration.ORIENTATION_PORTRAIT) + MMKVKeys.GRID_COLUMNS_COUNT_PORTRAIT + else MMKVKeys.GRID_COLUMNS_COUNT_LANDSCAPE, columnsCount + ) + } + }, + modifier = Modifier.padding(6.dp) + ) { + Icon( + imageVector = Icons.Default.Remove, + contentDescription = stringResource(R.string.decrease) + ) + } + } + Button( + onClick = { + columnsCount++ + mmkv.encode( + if (orientation == Configuration.ORIENTATION_PORTRAIT) + MMKVKeys.GRID_COLUMNS_COUNT_PORTRAIT + else MMKVKeys.GRID_COLUMNS_COUNT_LANDSCAPE, columnsCount + ) + }, + modifier = Modifier.padding(6.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(R.string.increase) + ) + } + Button( + onClick = { + columnsCount = 1 + mmkv.encode( + if (orientation == Configuration.ORIENTATION_PORTRAIT) + MMKVKeys.GRID_COLUMNS_COUNT_PORTRAIT + else MMKVKeys.GRID_COLUMNS_COUNT_LANDSCAPE, columnsCount + ) + }, + modifier = Modifier.padding(6.dp) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(R.string.reset) + ) + } + Button( + onClick = { + context.requestedOrientation = + if (orientation == Configuration.ORIENTATION_PORTRAIT) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + }, + modifier = Modifier.padding(6.dp) + ) { + Icon( + imageVector = Icons.Default.ScreenRotation, + contentDescription = stringResource(R.string.rotate) + ) + } + } + LazyVerticalGrid( + columns = GridCells.Fixed(columnsCount), + modifier = Modifier.padding(horizontal = 6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + (1..100).forEach { + item { + Card( + modifier = Modifier.padding(6.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = it.toString()) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/HomeCompose.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/HomeCompose.kt index 5bc5dd5..248df00 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/HomeCompose.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/HomeCompose.kt @@ -2,7 +2,10 @@ package com.lyneon.cytoidinfoquerier.ui.compose import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.ExtendedFloatingActionButton @@ -24,7 +27,10 @@ fun HomeCompose() { Column { TopBar() Column( - modifier = Modifier.padding(6.dp), + modifier = Modifier + .fillMaxSize() + .padding(6.dp) + .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(6.dp) ) { AlertCard(message = stringResource(id = R.string.debug_declaration)) diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/ProfileCompose.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/ProfileCompose.kt index e392314..cb25386 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/ProfileCompose.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/ProfileCompose.kt @@ -1,180 +1,1064 @@ package com.lyneon.cytoidinfoquerier.ui.compose -import android.os.Looper +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.text.Spannable +import android.text.style.ForegroundColorSpan import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.LocalTextStyle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import coil.request.ImageRequest +import com.lyneon.cytoidinfoquerier.BaseApplication import com.lyneon.cytoidinfoquerier.R -import com.lyneon.cytoidinfoquerier.logic.network.NetRequest -import com.lyneon.cytoidinfoquerier.model.webapi.Profile -import com.lyneon.cytoidinfoquerier.tool.extension.setPrecision -import com.lyneon.cytoidinfoquerier.tool.extension.isValidCytoidID -import com.lyneon.cytoidinfoquerier.tool.extension.showDialog -import com.lyneon.cytoidinfoquerier.tool.extension.showToast +import com.lyneon.cytoidinfoquerier.data.model.graphql.ProfileGraphQL +import com.lyneon.cytoidinfoquerier.data.model.webapi.Comment +import com.lyneon.cytoidinfoquerier.data.model.webapi.ProfileWebapi +import com.lyneon.cytoidinfoquerier.json +import com.lyneon.cytoidinfoquerier.logic.DateParser +import com.lyneon.cytoidinfoquerier.logic.DateParser.formatToTimeString import com.lyneon.cytoidinfoquerier.ui.activity.MainActivity import com.lyneon.cytoidinfoquerier.ui.compose.component.AlertCard +import com.lyneon.cytoidinfoquerier.ui.compose.component.CollectionCard +import com.lyneon.cytoidinfoquerier.ui.compose.component.LevelCard +import com.lyneon.cytoidinfoquerier.ui.compose.component.RecordCard import com.lyneon.cytoidinfoquerier.ui.compose.component.TopBar +import com.lyneon.cytoidinfoquerier.util.extension.getImageRequestBuilderForCytoid +import com.lyneon.cytoidinfoquerier.util.extension.isValidCytoidID +import com.lyneon.cytoidinfoquerier.util.extension.setPrecision +import com.lyneon.cytoidinfoquerier.util.extension.showToast import com.microsoft.appcenter.crashes.Crashes +import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis +import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis +import com.patrykandpatrick.vico.compose.chart.Chart +import com.patrykandpatrick.vico.compose.chart.column.columnChart +import com.patrykandpatrick.vico.compose.chart.line.lineChart +import com.patrykandpatrick.vico.compose.component.lineComponent +import com.patrykandpatrick.vico.compose.component.marker.markerComponent +import com.patrykandpatrick.vico.compose.component.overlayingComponent +import com.patrykandpatrick.vico.compose.component.shapeComponent +import com.patrykandpatrick.vico.compose.component.textComponent +import com.patrykandpatrick.vico.compose.extension.indicatorSize +import com.patrykandpatrick.vico.compose.m3.style.m3ChartStyle +import com.patrykandpatrick.vico.compose.style.ProvideChartStyle +import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis +import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider +import com.patrykandpatrick.vico.core.component.shape.DashedShape +import com.patrykandpatrick.vico.core.component.shape.Shapes +import com.patrykandpatrick.vico.core.dimensions.MutableDimensions +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer +import com.patrykandpatrick.vico.core.entry.entryOf +import com.patrykandpatrick.vico.core.extension.appendCompat +import com.patrykandpatrick.vico.core.extension.transformToSpannable +import com.patrykandpatrick.vico.core.marker.MarkerLabelFormatter +import com.tencent.mmkv.MMKV +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString import kotlin.concurrent.thread +import kotlin.time.Duration.Companion.milliseconds -lateinit var profile: Profile +val chartEntryModelProducer = ChartEntryModelProducer() +@SuppressLint("MutableCollectionMutableState") @Composable fun ProfileCompose() { val context = LocalContext.current as MainActivity + val mmkv = MMKV.defaultMMKV() + var profileGraphQL by remember { + mutableStateOf(ProfileGraphQL.getDefaultInstance()) + } + var profileWebapi by remember { + mutableStateOf(ProfileWebapi.getDefaultInstance()) + } + var comments by remember { + mutableStateOf(ArrayList()) + } var cytoidID by remember { mutableStateOf("") } var isQueryingFinished by remember { mutableStateOf(false) } var textFieldIsError by remember { mutableStateOf(false) } var textFieldIsEmpty by remember { mutableStateOf(false) } + var hideInput by remember { mutableStateOf(false) } + var querySettingsMenuIsExpanded by remember { mutableStateOf(false) } + var ignoreCache by remember { mutableStateOf(false) } + var keep2DecimalPlace by remember { mutableStateOf(true) } + var error by remember { mutableStateOf("") } Column { - TopBar(title = stringResource(id = R.string.profile)) + TopBar( + title = stringResource(id = R.string.profile), + additionalActions = { + if (hideInput) IconButton(onClick = { hideInput = false }) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = stringResource(id = R.string.unfold) + ) + } + } + ) Column( Modifier.padding(6.dp, 6.dp, 6.dp) ) { - Column { - TextField( - isError = textFieldIsError or textFieldIsEmpty, - value = cytoidID, - onValueChange = { - cytoidID = it - textFieldIsError = !it.isValidCytoidID() - textFieldIsEmpty = it.isEmpty() - }, - label = { Text(text = stringResource(id = R.string.playerName)) }, - modifier = Modifier.fillMaxWidth(), - trailingIcon = { - TextButton(onClick = { - if (cytoidID.isEmpty()) { - context.getString(R.string.empty_cytoidID) - .showToast() - textFieldIsEmpty = true - } else if (!cytoidID.isValidCytoidID()) { - context.getString(R.string.invalid_cytoidID) - .showToast() - textFieldIsError = true - } else { - textFieldIsError = false - isQueryingFinished = false - "开始查询$cytoidID".showToast() - thread { - try { - profile = NetRequest.getProfile(cytoidID) - Looper.prepare() - "查询${cytoidID}完成".showToast() - isQueryingFinished = true - } catch (e: Exception) { - e.printStackTrace() - Crashes.trackError(e) - Looper.prepare() - e.stackTraceToString() - .showDialog(context, "查询失败") + AnimatedVisibility(visible = !hideInput) { + Column { + TextField( + isError = textFieldIsError or textFieldIsEmpty, + value = cytoidID, + onValueChange = { + cytoidID = it + textFieldIsError = !it.isValidCytoidID() + textFieldIsEmpty = it.isEmpty() + }, + label = { Text(text = stringResource(id = R.string.playerName)) }, + modifier = Modifier.fillMaxWidth(), + trailingIcon = { + Row( + Modifier.padding(horizontal = 6.dp) + ) { + IconButton(onClick = { hideInput = true }) { + Icon( + imageVector = Icons.Default.KeyboardArrowUp, + contentDescription = stringResource(id = R.string.fold) + ) + } + IconButton(onClick = { querySettingsMenuIsExpanded = true }) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = stringResource(id = R.string.querySettings) + ) + DropdownMenu( + expanded = querySettingsMenuIsExpanded, + onDismissRequest = { + querySettingsMenuIsExpanded = false + } + ) { + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.ignore_cache)) + Checkbox( + checked = ignoreCache, + onCheckedChange = { + ignoreCache = it + } + ) + } + }, + onClick = { ignoreCache = !ignoreCache } + ) + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.keep_2_decimal_places)) + Checkbox( + checked = keep2DecimalPlace, + onCheckedChange = { + keep2DecimalPlace = it + } + ) + } + }, + onClick = { keep2DecimalPlace = !keep2DecimalPlace } + ) } } + TextButton(onClick = { + if (cytoidID.isEmpty()) { + context.getString(R.string.empty_cytoidID) + .showToast() + textFieldIsEmpty = true + } else if (!cytoidID.isValidCytoidID()) { + context.getString(R.string.invalid_cytoidID) + .showToast() + textFieldIsError = true + } else { + textFieldIsError = false + isQueryingFinished = false + if (System.currentTimeMillis() - mmkv.decodeLong( + "lastQueryProfileTime_${cytoidID}", + -1 + ) <= (6 * 60 * 60 * 1000) && !ignoreCache + ) { + val profileGraphQLString = + mmkv.decodeString("profileGraphQLString_${cytoidID}") + val profileWebapiString = + mmkv.decodeString("profileWebapiString_${cytoidID}") + val commentsString = + mmkv.decodeString("commentsString_${cytoidID}") + if (profileGraphQLString != null) profileGraphQL = + json.decodeFromString(profileGraphQLString) + else { + error = "Local cache profile data is null" + return@TextButton + } + if (profileWebapiString != null) profileWebapi = + json.decodeFromString(profileWebapiString) + else { + error = "Local cache profile data is null" + return@TextButton + } + comments = + if (commentsString != null) + json.decodeFromString(commentsString) + else arrayListOf() + isQueryingFinished = true + "6小时内有查询记录,使用已缓存的数据".showToast() + } else { + "开始查询$cytoidID".showToast() + thread { + val job = Job() + CoroutineScope(job).launch { + try { + val profiles = + awaitAll( + async { ProfileGraphQL.get(cytoidID) }, + async { ProfileWebapi.get(cytoidID) } + ) + profileGraphQL = + profiles[0] as ProfileGraphQL + profileWebapi = profiles[1] as ProfileWebapi + comments = + async { Comment.get(profileGraphQL.data.profile.user.id) }.await() + isQueryingFinished = true + mmkv.run { + encode( + "lastQueryProfileTime_${cytoidID}", + System.currentTimeMillis() + ) + encode( + "profileGraphQLString_${cytoidID}", + json.encodeToString(profileGraphQL) + ) + encode( + "profileWebapiString_${cytoidID}", + json.encodeToString(profileWebapi) + ) + encode( + "commentsString_${cytoidID}", + json.encodeToString(comments) + ) + } + } catch (e: Exception) { + error = e.stackTraceToString() + Crashes.trackError(e) + } + } + } + } + } + }) { + Text(text = stringResource(id = R.string.query)) + } } - }) { - Text(text = stringResource(id = R.string.query)) + }, + singleLine = true + ) + AnimatedVisibility(visible = textFieldIsError) { + Text( + text = stringResource(id = R.string.invalid_cytoidID), + color = Color.Red + ) + } + AnimatedVisibility(visible = textFieldIsEmpty) { + Text( + text = stringResource(id = R.string.empty_cytoidID), + color = Color.Red + ) + } + } + } + if (error.isNotEmpty()) { + AlertCard(message = error, modifier = Modifier.padding(vertical = 6.dp)) + } else { + AnimatedVisibility(visible = isQueryingFinished) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + item { + HeaderBar( + profileWebapi = profileWebapi, + keep2DecimalPlace = keep2DecimalPlace + ) + } + item { BiographyCard(profileGraphQL = profileGraphQL) } + item { BadgesCard(profileGraphQL = profileGraphQL) } + item { + RecentRecordsCard( + profileGraphQL = profileGraphQL, + keep2DecimalPlace = keep2DecimalPlace + ) + } + item { + DetailsCard( + profileWebapi = profileWebapi, + keep2DecimalPlace = keep2DecimalPlace + ) } - }, - singleLine = true + item { CollectionsCard(profileGraphQL = profileGraphQL) } + item { LevelsCard(profileGraphQL = profileGraphQL) } + item { CommentsColumn(comments = comments) } + } + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun HeaderBar(profileWebapi: ProfileWebapi, keep2DecimalPlace: Boolean) { + Row { + AsyncImage( + model = getImageRequestBuilderForCytoid(profileWebapi.user.avatar.large) + .build(), + contentDescription = profileWebapi.user.uid, + modifier = Modifier + .clip(CircleShape) + .clickable { + BaseApplication.context.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://cytoid.io/profile/${profileWebapi.user.uid}") + ) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addCategory(Intent.CATEGORY_BROWSABLE) + ) + } + ) + Spacer(modifier = Modifier.width(6.dp)) + Column { + Text( + text = profileWebapi.user.uid, + style = MaterialTheme.typography.titleLarge + ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = "Lv. ${profileWebapi.exp.currentLevel}", + color = Color.Black, + modifier = Modifier + .background(Color(0xFF9EB3FF), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) ) - AnimatedVisibility(visible = textFieldIsError) { + Text( + text = "Rating ${ + profileWebapi.rating.run { + if (keep2DecimalPlace) setPrecision(2) else this + } + }", + color = Color.Black, + modifier = Modifier + .background(Color(0xFF6AF5FF), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + profileWebapi.tier?.let { + val backgroundColorList = + it.colorPalette.background.split(",").run { + listOf( + Color(android.graphics.Color.parseColor(this[0])), + Color(android.graphics.Color.parseColor(this[1])) + ) + } Text( - text = stringResource(id = R.string.invalid_cytoidID), - color = Color.Red + text = it.name, + color = Color.White, + modifier = Modifier + .background( + Brush.linearGradient( + backgroundColorList, + Offset.Infinite, + Offset.Zero + ) + ) + .padding(horizontal = 6.dp) ) } - AnimatedVisibility(visible = textFieldIsEmpty) { + } + } + } +} + +@Composable +private fun BiographyCard(profileGraphQL: ProfileGraphQL) { + Card( + Modifier.fillMaxWidth() + ) { + Column( + Modifier.padding(6.dp) + ) { + Row { + Icon( + imageVector = Icons.Default.DateRange, + contentDescription = null + ) + Text( + text = "注册于${ + DateParser.parseISO8601Date(profileGraphQL.data.profile.user.registrationDate) + .formatToTimeString() + },${ + (System.currentTimeMillis() - DateParser.parseISO8601Date(profileGraphQL.data.profile.user.registrationDate).time) + .milliseconds.inWholeDays + }天前" + ) + } + if (profileGraphQL.data.profile.bio.isNotEmpty()) { + var folded by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { Text( - text = stringResource(id = R.string.empty_cytoidID), - color = Color.Red + text = stringResource(R.string.biography), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.align(Alignment.CenterVertically) ) + IconButton(onClick = { folded = !folded }) { + Icon( + imageVector = if (folded) Icons.AutoMirrored.Filled.KeyboardArrowRight else Icons.Default.KeyboardArrowDown, + contentDescription = if (folded) { + stringResource(R.string.unfold) + } else { + stringResource(R.string.fold) + } + ) + } + } + AnimatedVisibility(visible = !folded) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Spacer(modifier = Modifier.height(6.dp)) + Text(text = profileGraphQL.data.profile.bio) + } } } - AlertCard(message = stringResource(id = R.string.todo)) - AnimatedVisibility(visible = isQueryingFinished && ::profile.isInitialized) { - Column { - Row { - AsyncImage( - model = ImageRequest.Builder(context) - .data(profile.user.avatar.original) - .crossfade(true) - .setHeader( - "User-Agent", - "CytoidClient/2.1.1" + } + } +} + +@Composable +private fun BadgesCard(profileGraphQL: ProfileGraphQL) { + Card { + Column( + Modifier.padding(6.dp) + ) { + var folded by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "${stringResource(R.string.badge)}(共${profileGraphQL.data.profile.badges.size}个)", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.align(Alignment.CenterVertically) + ) + IconButton(onClick = { folded = !folded }) { + Icon( + imageVector = if (folded) Icons.AutoMirrored.Filled.KeyboardArrowRight else Icons.Default.KeyboardArrowDown, + contentDescription = if (folded) { + stringResource(R.string.unfold) + } else { + stringResource(R.string.fold) + } + ) + } + } + AnimatedVisibility(visible = !folded) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + profileGraphQL.data.profile.badges.forEach { + Text(text = it.title) + Text(text = it.description) + } + } + } + } + } +} + +@Composable +private fun RecentRecordsCard(profileGraphQL: ProfileGraphQL, keep2DecimalPlace: Boolean) { + Card { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(6.dp) + ) { + var folded by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "最新游玩纪录(共${profileGraphQL.data.profile.recentRecords.size}个)", + modifier = Modifier + .padding(6.dp) + .align(Alignment.CenterVertically), + style = MaterialTheme.typography.titleLarge, + ) + IconButton(onClick = { folded = !folded }) { + Icon( + imageVector = if (folded) Icons.AutoMirrored.Filled.KeyboardArrowRight else Icons.Default.KeyboardArrowDown, + contentDescription = if (folded) { + stringResource(R.string.unfold) + } else { + stringResource(R.string.fold) + } + ) + } + } + AnimatedVisibility(visible = !folded) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + profileGraphQL.data.profile.recentRecords.forEach { + RecordCard(record = it, keep2DecimalPlaces = keep2DecimalPlace) + } + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun DetailsCard(profileWebapi: ProfileWebapi, keep2DecimalPlace: Boolean) { + var tabIndex by remember { mutableIntStateOf(0) } + val timeSeries = profileWebapi.timeSeries.apply { + sortBy { it.date.replace("-", "").toInt() } + } + chartEntryModelProducer.setEntries(timeSeries.map { + entryOf( + timeSeries.indexOf(it), + when (tabIndex) { + 0 -> it.rating.run { + if (keep2DecimalPlace) setPrecision(2).toFloat() else this + } + + 1 -> it.count + 2 -> (it.accuracy * 100).run { + if (keep2DecimalPlace) setPrecision(2).toFloat() else this + } + + else -> -1 + } + ) + }) + + Card( + Modifier.fillMaxWidth() + ) { + Column( + Modifier.padding(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row { + Column( + Modifier.weight(1f) + ) { + Text(text = "总游玩次数") + Text( + text = profileWebapi.activities.totalRankedPlays.toString(), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + Column( + Modifier.weight(1f) + ) { + Text(text = "总Note数") + Text( + text = profileWebapi.activities.clearedNotes.toString(), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + } + Row { + Column( + Modifier.weight(1f) + ) { + Text(text = "最高连击数") + Text( + text = profileWebapi.activities.maxCombo.toString(), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + Column( + Modifier.weight(1f) + ) { + Text(text = "平均精准度") + Text( + text = "${ + (profileWebapi.activities.averageRankedAccuracy * 100).run { + if (keep2DecimalPlace) setPrecision(2) + else this + } + }%", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + } + Row { + Column( + Modifier.weight(1f) + ) { + Text(text = "总分数") + Text( + text = profileWebapi.activities.totalRankedScore.toString(), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + Column( + Modifier.weight(1f) + ) { + val duration = + (profileWebapi.activities.totalPlayTime * 1000).toLong().milliseconds + val days = duration.inWholeDays + val hours = duration.inWholeHours - duration.inWholeDays * 24 + val minutes = duration.inWholeMinutes - duration.inWholeHours * 60 + val seconds = duration.inWholeSeconds - duration.inWholeMinutes * 60 + + Text(text = "总游玩时间") + Text( + text = (if (days != 0.toLong()) "${days}天" else "") + + (if (hours != 0.toLong()) "${hours}时" else "") + + (if (minutes != 0.toLong()) "${minutes}分" else "") + + (if (seconds != 0.toLong()) "${seconds}秒" else ""), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + } + Text( + text = "成绩分布", + style = MaterialTheme.typography.titleLarge + ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = "MAX ${profileWebapi.grade.MAX}", + color = Color.Black, + modifier = Modifier + .background(Color(0xFFFFCC00), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "SSS ${profileWebapi.grade.SSS}", + color = Color.Black, + modifier = Modifier + .background(Color(0xFF08CFFF), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "SS ${profileWebapi.grade.SS}", + color = Color.White, + modifier = Modifier + .background(Color(0xFF3F4561), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "S ${profileWebapi.grade.S}", + color = Color.White, + modifier = Modifier + .background(Color(0xFF3F4561), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "AA ${profileWebapi.grade.AA}", + color = Color.White, + modifier = Modifier + .background(Color(0xFF3F4561), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "A ${profileWebapi.grade.A}", + color = Color.White, + modifier = Modifier + .background(Color(0xFF3F4561), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "B ${profileWebapi.grade.B}", + color = Color.White, + modifier = Modifier + .background(Color(0xFF3F4561), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "C ${profileWebapi.grade.C}", + color = Color.White, + modifier = Modifier + .background(Color(0xFF3F4561), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "D ${profileWebapi.grade.D}", + color = Color.White, + modifier = Modifier + .background(Color(0xFF3F4561), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + Text( + text = "F ${profileWebapi.grade.F}", + color = Color.White, + modifier = Modifier + .background(Color(0xFF3F4561), RoundedCornerShape(100)) + .padding(horizontal = 6.dp) + ) + } + TabRow( + selectedTabIndex = tabIndex, + containerColor = Color.Transparent, + ) { + Tab( + selected = tabIndex == 0, + onClick = { tabIndex = 0 } + ) { + Text( + text = "Rating", + modifier = Modifier.padding(6.dp) + ) + } + Tab( + selected = tabIndex == 1, + onClick = { tabIndex = 1 } + ) { + Text( + text = "游玩次数", + modifier = Modifier.padding(6.dp) + ) + } + Tab( + selected = tabIndex == 2, + onClick = { tabIndex = 2 } + ) { + Text( + text = "平均精准度", + modifier = Modifier.padding(6.dp) + ) + } + } + ProvideChartStyle(m3ChartStyle()) { + Chart( + chart = if (tabIndex == 1) columnChart( + axisValuesOverrider = AxisValuesOverrider.adaptiveYValues(1f) + ) else lineChart( + axisValuesOverrider = AxisValuesOverrider.adaptiveYValues(1f) + ), + chartModelProducer = chartEntryModelProducer, + bottomAxis = rememberBottomAxis( + valueFormatter = { value, _ -> + (if (value.toInt() < timeSeries.size) + timeSeries[value.toInt()].date.replace("-", "w") + else "null").substring(2) + }, + labelRotationDegrees = 90f + ), + startAxis = rememberStartAxis( + valueFormatter = { value, _ -> + when (tabIndex) { + 0 -> value.run { + if (keep2DecimalPlace) setPrecision(2) else this + }.toString() + + 1 -> value.toInt().toString() + 2 -> "${ + value.run { + if (keep2DecimalPlace) setPrecision(2) else this + } + }%" + + else -> "Error" + } + }, + horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside + ), + marker = markerComponent( + label = textComponent( + background = shapeComponent( + shape = Shapes.pillShape, + color = MaterialTheme.colorScheme.surface + ), + padding = MutableDimensions(6f, 6f), + margins = MutableDimensions(0f, 0f, 0f, 6f) + ), + indicator = overlayingComponent( + outer = shapeComponent( + shape = Shapes.pillShape, + color = MaterialTheme.colorScheme.secondaryContainer + ), + inner = shapeComponent( + shape = Shapes.pillShape, + color = MaterialTheme.colorScheme.primary + ), + innerPaddingAll = 6.dp + ), + guideline = lineComponent( + color = Color(0x80808080), + thickness = 2.dp, + shape = DashedShape(Shapes.pillShape) + ) + ).apply { + indicatorSize = 16.dp + this.labelFormatter = MarkerLabelFormatter { markedEntries, _ -> + markedEntries.transformToSpannable { model -> + appendCompat( + profileWebapi.timeSeries[model.index].date.replace( + "-", + "年第" + ) + "周;", + ForegroundColorSpan(model.color), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) - .crossfade(true) - .error(R.drawable.sayakacry) - .build(), - contentDescription = profile.user.uid, + appendCompat( + when (tabIndex) { + 0 -> model.entry.y.run { + if (keep2DecimalPlace) setPrecision(2) else this.toString() + } + + 1 -> model.entry.y.toInt().toString() + 2 -> model.entry.y.run { + if (keep2DecimalPlace) setPrecision(2) else this.toString() + } + "%" + + else -> "" + }, + ForegroundColorSpan(model.color), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + } + ) + } + } + } +} + +@Composable +private fun CollectionsCard(profileGraphQL: ProfileGraphQL) { + Card { + Column( + modifier = Modifier.padding(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + var folded by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "合集(共${profileGraphQL.data.profile.user.collectionsCount}个)", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.align(Alignment.CenterVertically) + ) + IconButton(onClick = { folded = !folded }) { + Icon( + imageVector = if (folded) Icons.AutoMirrored.Filled.KeyboardArrowRight else Icons.Default.KeyboardArrowDown, + contentDescription = if (folded) { + stringResource(R.string.unfold) + } else { + stringResource(R.string.fold) + } + ) + } + } + + AnimatedVisibility(visible = !folded) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + profileGraphQL.data.profile.user.collections.forEach { + CollectionCard(collection = it) + } + } + } + } + } +} + +@Composable +private fun LevelsCard(profileGraphQL: ProfileGraphQL) { + Card { + Column( + modifier = Modifier.padding(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + var folded by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "上传的关卡(共${profileGraphQL.data.profile.user.levelsCount}个)", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.align(Alignment.CenterVertically) + ) + IconButton(onClick = { folded = !folded }) { + Icon( + imageVector = if (folded) Icons.AutoMirrored.Filled.KeyboardArrowRight else Icons.Default.KeyboardArrowDown, + contentDescription = if (folded) { + stringResource(R.string.unfold) + } else { + stringResource(R.string.fold) + } + ) + } + } + + AnimatedVisibility(visible = !folded) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + profileGraphQL.data.profile.user.levels.forEach { + LevelCard(level = it) + } + } + } + } + } +} + +@Composable +private fun CommentsColumn(comments: ArrayList) { + Column( + modifier = Modifier.padding(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + var folded by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "留言(共${comments.size}个)", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.align(Alignment.CenterVertically) + ) + IconButton(onClick = { folded = !folded }) { + Icon( + imageVector = if (folded) Icons.AutoMirrored.Filled.KeyboardArrowRight else Icons.Default.KeyboardArrowDown, + contentDescription = if (folded) { + stringResource(R.string.unfold) + } else { + stringResource(R.string.fold) + } + ) + } + } + + AnimatedVisibility(visible = !folded) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + comments.forEach { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + AsyncImage( + model = getImageRequestBuilderForCytoid(it.owner.avatar.medium).build(), + contentDescription = it.owner.uid, modifier = Modifier + .weight(1f) .clip(CircleShape) - .width(160.dp) ) - Column { - Text( - text = profile.user.uid, - fontSize = LocalTextStyle.current.fontSize.times(2) - ) - Row { - profile.tier?.run { - val tierBackgroundColors = mutableListOf() - this.colorPalette.background.split(",").forEach { - if (it.startsWith("#")) tierBackgroundColors.add( - Color(android.graphics.Color.parseColor(it)) - ) - } + Card( + Modifier.weight(9f) + ) { + Column( + Modifier.padding(6.dp) + ) { + Row { Text( - text = this.name, - modifier = Modifier - .background( - brush = Brush.linearGradient(tierBackgroundColors), - shape = RoundedCornerShape(6.dp) - ) - .padding(6.dp) + text = it.owner.uid, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "${ + (System.currentTimeMillis() - DateParser.parseISO8601Date( + it.date + ).time) + .milliseconds.inWholeDays + }天前" ) } Text( - text = "Level ${profile.exp.currentLevel}", - modifier = Modifier - .background( - color = Color.LightGray, - shape = RoundedCornerShape(6.dp) - ) - .padding(6.dp) - ) - Text( - text = "Rating ${profile.rating.setPrecision(2)}", - modifier = Modifier - .background( - color = Color.LightGray, - shape = RoundedCornerShape(6.dp) - ) - .padding(6.dp) + text = it.content, + style = MaterialTheme.typography.bodyMedium ) } } diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/SettingsCompose.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/SettingsCompose.kt index 92045e6..6b1959d 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/SettingsCompose.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/SettingsCompose.kt @@ -1,21 +1,25 @@ package com.lyneon.cytoidinfoquerier.ui.compose -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Process import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Card +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.StayPrimaryPortrait +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult.* +import androidx.compose.material3.SnackbarResult.ActionPerformed +import androidx.compose.material3.SnackbarResult.Dismissed import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,23 +30,27 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.lyneon.cytoidinfoquerier.BaseApplication import com.lyneon.cytoidinfoquerier.BaseApplication.Companion.context import com.lyneon.cytoidinfoquerier.R +import com.lyneon.cytoidinfoquerier.data.constant.MMKVKeys +import com.lyneon.cytoidinfoquerier.data.constant.NavRoute import com.lyneon.cytoidinfoquerier.ui.compose.component.TopBar -import com.microsoft.appcenter.crashes.Crashes import com.tencent.mmkv.MMKV import kotlinx.coroutines.launch -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable -fun SettingsCompose() { +fun SettingsCompose(navController: NavController) { val mmkv = MMKV.defaultMMKV() val scope = rememberCoroutineScope() var enableAppCenter by remember { - mutableStateOf(mmkv.decodeBool(SettingsMMKVKeys.enableAppCenter, true)) + mutableStateOf(mmkv.decodeBool(MMKVKeys.ENABLE_APP_CENTER, true)) } val snackbarHostState = remember { SnackbarHostState() } Scaffold( @@ -55,54 +63,49 @@ fun SettingsCompose() { Column( modifier = Modifier .fillMaxSize() - .padding(paddingValues), - verticalArrangement = Arrangement.spacedBy(6.dp) + .padding(paddingValues) ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(6.dp) - ) { + SettingsItem(onClick = { + enableAppCenter = !enableAppCenter + mmkv.encode(MMKVKeys.ENABLE_APP_CENTER, enableAppCenter) + scope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + val result = snackbarHostState.showSnackbar( + context.getString(R.string.changes_need_restart_to_enable), + context.getString(R.string.restart), + true, + SnackbarDuration.Short + ) + when (result) { + ActionPerformed -> BaseApplication.restartApp() + Dismissed -> {} + } + } + + }, hideDivider = true) { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() - .clickable { - enableAppCenter = !enableAppCenter - mmkv.encode(SettingsMMKVKeys.enableAppCenter, enableAppCenter) - scope.launch { - snackbarHostState.currentSnackbarData?.dismiss() - val result = snackbarHostState.showSnackbar( - context.getString(R.string.changes_need_restart_to_enable), - context.getString(R.string.restart), - true, - SnackbarDuration.Short - ) - when (result) { - ActionPerformed -> { - val intent = - context.packageManager.getLaunchIntentForPackage(context.packageName) - if (intent != null) { - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - context.startActivity(intent) - } - Process.killProcess(Process.myPid()) - } - - Dismissed -> {} - } - } - } - .padding(6.dp) + .padding(16.dp) ) { - Text( - text = stringResource(id = R.string.enable_app_center), + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.align(Alignment.CenterVertically) - ) + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_app_center), + contentDescription = stringResource(id = R.string.enable_app_center) + ) + Text( + text = stringResource(id = R.string.enable_app_center), + modifier = Modifier.align(Alignment.CenterVertically) + ) + } Switch( checked = enableAppCenter, onCheckedChange = { checked -> enableAppCenter = checked - mmkv.encode(SettingsMMKVKeys.enableAppCenter, checked) + mmkv.encode(MMKVKeys.ENABLE_APP_CENTER, checked) scope.launch { snackbarHostState.currentSnackbarData?.dismiss() when (snackbarHostState.showSnackbar( @@ -111,16 +114,7 @@ fun SettingsCompose() { true, SnackbarDuration.Short )) { - ActionPerformed -> { - val intent = - context.packageManager.getLaunchIntentForPackage(context.packageName) - if (intent != null) { - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - context.startActivity(intent) - } - Process.killProcess(Process.myPid()) - } - + ActionPerformed -> BaseApplication.restartApp() Dismissed -> {} } } @@ -128,80 +122,120 @@ fun SettingsCompose() { ) } } - Card( - modifier = Modifier - .fillMaxWidth() - .padding(6.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - scope.launch { - snackbarHostState.currentSnackbarData?.dismiss() - when (snackbarHostState.showSnackbar( - context.getString(R.string.delete_confirm), - context.getString(R.string.confirm), - true, - SnackbarDuration.Short - )) { - Dismissed -> {} - ActionPerformed -> { - val cacheDir = context.externalCacheDir?.listFiles() - cacheDir?.run { - for (file in cacheDir) { - file.delete() - } - } - } + SettingsItem(onClick = { + scope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + when (snackbarHostState.showSnackbar( + context.getString(R.string.delete_confirm), + context.getString(R.string.confirm), + true, + SnackbarDuration.Short + )) { + Dismissed -> {} + ActionPerformed -> { + val cacheDir = context.externalCacheDir?.listFiles() + cacheDir?.run { + for (file in cacheDir) { + file.delete() } } } - .padding(6.dp) + } + } + }) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(id = R.string.delete_cache_image) + ) Text( text = stringResource(id = R.string.delete_cache_image), modifier = Modifier.align(Alignment.CenterVertically) ) } } - Card( - modifier = Modifier - .fillMaxWidth() - .padding(6.dp) - ) { + SettingsItem(onClick = { + scope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + when (snackbarHostState.showSnackbar( + context.getString(R.string.testCrash), + context.getString(R.string.confirm), + true, + SnackbarDuration.Short + )) { + Dismissed -> {} + ActionPerformed -> throw Exception("This is a crash for test!") + } + } + }) { Row( modifier = Modifier .fillMaxWidth() - .clickable { - scope.launch { - snackbarHostState.currentSnackbarData?.dismiss() - when (snackbarHostState.showSnackbar( - context.getString(R.string.testCrash), - context.getString(R.string.confirm), - true, - SnackbarDuration.Short - )) { - Dismissed -> {} - ActionPerformed -> { - Crashes.generateTestCrash() - } - } - } - } - .padding(6.dp) + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) ) { + Icon( + imageVector = Icons.Default.BugReport, + contentDescription = stringResource(id = R.string.testCrash) + ) Text( text = stringResource(id = R.string.testCrash), modifier = Modifier.align(Alignment.CenterVertically) ) } } - + SettingsItem(onClick = { + navController.navigate(NavRoute.gridColumnsSetting) + }) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterVertically), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + imageVector = Icons.Default.StayPrimaryPortrait, + contentDescription = stringResource(R.string.grid_columns_count) + ) + Text( + text = stringResource(id = R.string.grid_columns_count) + ) + } + Text( + text = "竖屏:${ + mmkv.decodeInt(MMKVKeys.GRID_COLUMNS_COUNT_PORTRAIT, 1) + } 横屏:${ + mmkv.decodeInt(MMKVKeys.GRID_COLUMNS_COUNT_LANDSCAPE, 1) + }", + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(16.dp) + ) + } + } } } } -object SettingsMMKVKeys { - const val enableAppCenter = "enableAppCenter" +@Composable +fun SettingsItem( + hideDivider: Boolean = false, + onClick: () -> Unit, + content: @Composable () -> Unit +) { + Column { + if (!hideDivider) HorizontalDivider() + Box(modifier = Modifier.clickable { onClick() }) { + content() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/AlertCard.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/AlertCard.kt index fdd0511..bdf8192 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/AlertCard.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/AlertCard.kt @@ -1,12 +1,11 @@ package com.lyneon.cytoidinfoquerier.ui.compose.component -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -17,18 +16,21 @@ import androidx.compose.ui.unit.dp @Composable fun AlertCard( - icon: ImageVector? = Icons.Rounded.Info, + modifier: Modifier = Modifier, + icon: ImageVector? = Icons.Default.Info, message: String ) { - Card { - Box(modifier = Modifier.padding(16.dp)) { - Row { - icon?.let { - Icon(imageVector = it, contentDescription = message) - Spacer(modifier = Modifier.width(6.dp)) - } - Text(text = message) + Card( + modifier = modifier + ) { + Row( + modifier = Modifier.padding(16.dp) + ) { + icon?.let { + Icon(imageVector = it, contentDescription = message) + Spacer(modifier = Modifier.width(6.dp)) } + Text(text = message) } } } \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/CollectionCard.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/CollectionCard.kt new file mode 100644 index 0000000..ba946d1 --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/CollectionCard.kt @@ -0,0 +1,71 @@ +package com.lyneon.cytoidinfoquerier.ui.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.lyneon.cytoidinfoquerier.data.model.graphql.ProfileGraphQL +import com.lyneon.cytoidinfoquerier.util.extension.getImageRequestBuilderForCytoid +import com.patrykandpatrick.vico.compose.component.shape.composeShape +import com.patrykandpatrick.vico.core.component.shape.Shapes + +@Composable +fun CollectionCard(collection: ProfileGraphQL.ProfileData.Profile.User.CollectionUserListing) { + Card { + Box { + AsyncImage( + model = getImageRequestBuilderForCytoid(collection.cover.thumbnail).build(), + contentDescription = collection.title, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth + ) + Column( + Modifier + .align(Alignment.BottomStart) + .fillMaxWidth() + .background(Color(0x80000000)) + .padding(6.dp) + ) { + Text( + text = collection.title, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = collection.slogan, + color = Color.White, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + Text( + text = "${collection.levelCount}个关卡", + color = Color.White, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .background( + Color(0xFF414558), + Shapes.pillShape.composeShape() + ) + .padding(6.dp) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/LevelCard.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/LevelCard.kt new file mode 100644 index 0000000..a642cb1 --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/LevelCard.kt @@ -0,0 +1,279 @@ +package com.lyneon.cytoidinfoquerier.ui.compose.component + +import android.content.ContentValues +import android.content.Intent +import android.net.Uri +import android.os.Looper +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import coil.compose.AsyncImage +import com.lyneon.cytoidinfoquerier.BaseActivity +import com.lyneon.cytoidinfoquerier.BaseApplication +import com.lyneon.cytoidinfoquerier.R +import com.lyneon.cytoidinfoquerier.data.CytoidDeepLink +import com.lyneon.cytoidinfoquerier.data.constant.CytoidColors +import com.lyneon.cytoidinfoquerier.data.model.graphql.ProfileGraphQL +import com.lyneon.cytoidinfoquerier.util.extension.getImageRequestBuilderForCytoid +import com.lyneon.cytoidinfoquerier.util.extension.saveIntoMediaStore +import com.lyneon.cytoidinfoquerier.util.extension.showDialog +import com.lyneon.cytoidinfoquerier.util.extension.showToast +import com.lyneon.cytoidinfoquerier.util.extension.toBitmap +import com.patrykandpatrick.vico.compose.component.shape.composeShape +import com.patrykandpatrick.vico.core.component.shape.Shapes +import java.net.URL +import java.util.Locale +import kotlin.concurrent.thread + +@androidx.annotation.OptIn(UnstableApi::class) +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun LevelCard(level: ProfileGraphQL.ProfileData.Profile.User.UserLevel) { + val context = LocalContext.current as BaseActivity + var mediaPlayerState by remember { mutableIntStateOf(Player.STATE_IDLE) } + val exoPlayer = ExoPlayer.Builder(BaseApplication.context).build().apply { + addListener(object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + Player.STATE_ENDED -> mediaPlayerState = Player.STATE_ENDED + Player.STATE_BUFFERING -> mediaPlayerState = Player.STATE_BUFFERING + Player.STATE_IDLE -> mediaPlayerState = Player.STATE_IDLE + Player.STATE_READY -> mediaPlayerState = Player.STATE_READY + } + } + }) + } + var levelDialogState by remember { mutableStateOf(false) } + + Card( + Modifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { + levelDialogState = true + } + ) + } + ) { + Box { + AsyncImage( + model = getImageRequestBuilderForCytoid(level.bundle.backgroundImage.thumbnail).build(), + contentDescription = level.title, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth + ) + Column( + Modifier + .align(Alignment.BottomStart) + .fillMaxWidth() + .background(Color(0x80000000)) + .padding(6.dp) + ) { + Text( + text = level.title, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + level.description?.let { description -> + Text( + text = description, + color = Color.White, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + Text( + text = level.metadata.artist.name, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + level.charts.forEach { chart -> + Text( + text = " ${ + chart.name + ?: chart.type.replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.getDefault() + ) else it.toString() + } + } ${chart.difficulty} ", + color = Color.White, + modifier = Modifier + .background( + Brush.linearGradient( + when (chart.type) { + "easy" -> CytoidColors.easyColor + "extreme" -> CytoidColors.extremeColor + else -> CytoidColors.hardColor + } + ), Shapes.pillShape.composeShape() + ) + .padding(6.dp) + ) + } + } + } + IconButton( + onClick = { + if (mediaPlayerState != Player.STATE_READY) { + val dataSourceFactory = + DefaultHttpDataSource.Factory() + .setDefaultRequestProperties(mapOf("User-Agent" to "CytoidClient/2.1.1")) + val mediaItem = MediaItem.Builder() + .setUri(Uri.parse(level.bundle.musicPreview ?: level.bundle.music)) + .build() + val internetAudioSource = + ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(mediaItem) + exoPlayer.setMediaSource(internetAudioSource) + exoPlayer.prepare() + exoPlayer.play() + } else { + exoPlayer.stop() + } + }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .background( + MaterialTheme.colorScheme.primaryContainer, + CircleShape + ) + ) { + if (mediaPlayerState == Player.STATE_BUFFERING) CircularProgressIndicator( + Modifier.padding(6.dp) + ) + else Icon( + imageVector = if (mediaPlayerState == Player.STATE_READY) Icons.Default.Stop else Icons.Default.PlayArrow, + contentDescription = "${if (mediaPlayerState == Player.STATE_READY) "停止" else "播放"}音乐预览" + ) + } + } + } + + if (levelDialogState) AlertDialog( + onDismissRequest = { levelDialogState = false }, + confirmButton = {}, + title = { + Text(text = level.title, style = MaterialTheme.typography.titleLarge) + }, + text = { + Column( + Modifier.verticalScroll(rememberScrollState()) + ) { + ListItem( + headlineContent = { Text(text = stringResource(id = R.string.view_in_cytoid)) }, + modifier = Modifier.clickable { + if (BaseApplication.cytoidIsInstalled) { + BaseApplication.context.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse(CytoidDeepLink.getCytoidLevelDeepLink(level.uid)) + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } else context + .getString(R.string.cytoid_is_not_installed) + .showToast() + } + ) + ListItem( + headlineContent = { Text(text = stringResource(id = R.string.view_in_cytoidIO)) }, + modifier = Modifier.clickable { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://cytoid.io/levels/${level.uid}") + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + BaseApplication.context.startActivity(intent) + } + ) + ListItem( + headlineContent = { Text(context.getString(R.string.save_illustration)) }, + modifier = Modifier.clickable { + context + .getString(R.string.saving_illustration) + .showToast() + thread { + kotlin + .runCatching { + URL(level.bundle.backgroundImage.original) + .toBitmap() + .saveIntoMediaStore( + context.contentResolver, + ContentValues() + ) + } + .onSuccess { + Looper.prepare() + context + .getString(R.string.saved_into_gallery) + .showToast() + } + .onFailure { e -> + e.printStackTrace() + context.runOnUiThread { + e + .stackTraceToString() + .showDialog( + context, + context.getString(R.string.fail) + ) + } + } + } + } + ) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/RecordCard.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/RecordCard.kt index b1b6d56..e2f8ed5 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/RecordCard.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/RecordCard.kt @@ -1,35 +1,48 @@ package com.lyneon.cytoidinfoquerier.ui.compose.component -import android.app.AlertDialog +import android.annotation.SuppressLint import android.content.ContentValues -import android.content.DialogInterface import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Looper +import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -49,20 +62,27 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.ProgressiveMediaSource import coil.compose.AsyncImage -import coil.request.ImageRequest +import com.lyneon.cytoidinfoquerier.BaseActivity +import com.lyneon.cytoidinfoquerier.BaseApplication import com.lyneon.cytoidinfoquerier.R +import com.lyneon.cytoidinfoquerier.data.CytoidDeepLink +import com.lyneon.cytoidinfoquerier.data.model.graphql.UserRecord import com.lyneon.cytoidinfoquerier.logic.DateParser import com.lyneon.cytoidinfoquerier.logic.DateParser.formatToTimeString -import com.lyneon.cytoidinfoquerier.model.CytoidDeepLink -import com.lyneon.cytoidinfoquerier.model.graphql.UserRecord -import com.lyneon.cytoidinfoquerier.tool.extension.saveIntoClipboard -import com.lyneon.cytoidinfoquerier.tool.extension.saveIntoMediaStore -import com.lyneon.cytoidinfoquerier.tool.extension.setPrecision -import com.lyneon.cytoidinfoquerier.tool.extension.showDialog -import com.lyneon.cytoidinfoquerier.tool.extension.showToast -import com.lyneon.cytoidinfoquerier.tool.extension.toBitmap -import com.lyneon.cytoidinfoquerier.ui.activity.MainActivity +import com.lyneon.cytoidinfoquerier.util.extension.getImageRequestBuilderForCytoid +import com.lyneon.cytoidinfoquerier.util.extension.saveIntoClipboard +import com.lyneon.cytoidinfoquerier.util.extension.saveIntoMediaStore +import com.lyneon.cytoidinfoquerier.util.extension.setPrecision +import com.lyneon.cytoidinfoquerier.util.extension.showDialog +import com.lyneon.cytoidinfoquerier.util.extension.showToast +import com.lyneon.cytoidinfoquerier.util.extension.toBitmap import com.microsoft.appcenter.crashes.Crashes import dev.shreyaspatil.capturable.Capturable import dev.shreyaspatil.capturable.controller.rememberCaptureController @@ -73,11 +93,30 @@ import java.net.URL import java.util.Locale import kotlin.concurrent.thread +@kotlin.OptIn(ExperimentalLayoutApi::class) +@OptIn(UnstableApi::class) +@SuppressLint("CheckResult") @Composable fun RecordCard(record: UserRecord, recordIndex: Int? = null, keep2DecimalPlaces: Boolean = true) { - val context = LocalContext.current as MainActivity + val context = LocalContext.current as BaseActivity val externalCacheStorageDir = context.externalCacheDir val captureController = rememberCaptureController() + var recordDialogState by remember { mutableStateOf(false) } + var copyRecordContentDialogState by remember { mutableStateOf(false) } + var mediaPlayerState by remember { mutableIntStateOf(Player.STATE_IDLE) } + val exoPlayer = ExoPlayer.Builder(BaseApplication.context).build().apply { + addListener(object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + Player.STATE_ENDED -> mediaPlayerState = Player.STATE_ENDED + Player.STATE_BUFFERING -> mediaPlayerState = Player.STATE_BUFFERING + Player.STATE_IDLE -> mediaPlayerState = Player.STATE_IDLE + Player.STATE_READY -> mediaPlayerState = Player.STATE_READY + } + } + }) + } + Capturable( controller = captureController, onCaptured = { imageBitmap: ImageBitmap?, throwable: Throwable? -> @@ -86,278 +125,152 @@ fun RecordCard(record: UserRecord, recordIndex: Int? = null, keep2DecimalPlaces: context.getString(R.string.saved).showToast() } throwable?.let { throw it } - }) { + } + ) { Card( Modifier - .padding(6.dp) .pointerInput(Unit) { detectTapGestures( onLongPress = { - AlertDialog - .Builder(context) - .setItems( - arrayOf( - context.getString(R.string.view_in_cytoid), - context.getString(R.string.copy_content), - context.getString(R.string.save_illustration), - context.getString(R.string.save_as_picture) + recordDialogState = true + } + ) + } + ) { + Column( + Modifier.padding(6.dp) + ) { + Box { + if (record.chart?.level != null) { + val level = record.chart.level + if (externalCacheStorageDir != null) { + val cacheBackgroundImagesDirectory = + File(externalCacheStorageDir.path + "/backgroundImage") + if (!cacheBackgroundImagesDirectory.exists()) cacheBackgroundImagesDirectory.mkdirs() + val cacheBackgroundImageFile = + File( + cacheBackgroundImagesDirectory, + level.uid + ) + if (cacheBackgroundImageFile.isFile) { + val input = FileInputStream(cacheBackgroundImageFile) + val bitmap = BitmapFactory.decodeStream(input) + Card { + Image( + painter = BitmapPainter(bitmap.asImageBitmap()), + contentDescription = level.title, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() ) - ) { _, i: Int -> - when (i) { - 0 -> try { - context.startActivity( - Intent( - Intent.ACTION_VIEW, - Uri.parse(CytoidDeepLink.getDeepLink(record.chart.level.uid)) - ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } else { + Card { + Box { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + var backgroundImageIsError by remember { + mutableStateOf( + false ) - } catch (e: Exception) { - e - .stackTraceToString() - .showDialog( - context, - context.getString(R.string.fail) - ) { - this.setPositiveButton(context.getString(R.string.confirm)) { dialogInterface: DialogInterface, _: Int -> - dialogInterface.dismiss() - } - } } - - 1 -> { - AlertDialog - .Builder(context) - .setTitle(context.resources.getString(R.string.choose_copy_content)) - .setItems( - arrayOf( - record.chart.level.title, - record.chart.level.uid, - "${ - record.chart.name - ?: record.chart.type.replaceFirstChar { - if (it.isLowerCase()) it.titlecase( - Locale.getDefault() - ) else it.toString() - } - } ${record.chart.difficulty}", - record.score.toString(), - "Mods:${record.mods}", - "${ - (record.accuracy * 100).run { - if (keep2DecimalPlaces) this.setPrecision( - 2 - ) else this - } - }% accuracy ${record.details.maxCombo} max combo", - "Rating ${ - record.rating.run { - if (keep2DecimalPlaces) this.setPrecision( - 2 - ) else this - } - }", - "Perfect ${record.details.perfect} Great ${record.details.great} Good ${record.details.good} Bad ${record.details.bad} Miss ${record.details.miss}", - DateParser - .parseISO8601Date(record.date) - .formatToTimeString(), - context.getString(R.string.all_contents), - "(仅调试)UserRecord对象" - ) - ) { _, j: Int -> - when (j) { - 0 -> record.chart.level.title.saveIntoClipboard() - 1 -> record.chart.level.uid.saveIntoClipboard() - 2 -> "${ - record.chart.name - ?: record.chart.type.replaceFirstChar { - if (it.isLowerCase()) it.titlecase( - Locale.getDefault() - ) else it.toString() - } - } ${record.chart.difficulty}".saveIntoClipboard() - - 3 -> record.score - .toString() - .saveIntoClipboard() - - 4 -> "Mods:${record.mods}".saveIntoClipboard() - - 5 -> "${ - (record.accuracy * 100).run { - if (keep2DecimalPlaces) this.setPrecision( - 2 - ) else this - } - }% accuracy ${record.details.maxCombo} max combo".saveIntoClipboard() - - 6 -> "Rating ${ - record.rating.run { - if (keep2DecimalPlaces) this.setPrecision( - 2 - ) else this - } - }".saveIntoClipboard() - - 7 -> "Perfect ${record.details.perfect} Great ${record.details.great} Good ${record.details.good} Bad ${record.details.bad} Miss ${record.details.miss}".saveIntoClipboard() - 8 -> DateParser - .parseISO8601Date(record.date) - .formatToTimeString() - .saveIntoClipboard() - - 9 -> record - .detailsString() - .saveIntoClipboard() - - 10 -> record - .toString() - .saveIntoClipboard() - } - - } - .create() - .show() - } - - 2 -> { - context - .getString(R.string.saving_illustration) - .showToast() - thread { - kotlin - .runCatching { - URL(record.chart.level.bundle.backgroundImage.original) - .toBitmap() - .saveIntoMediaStore( - context.contentResolver, - ContentValues() + Column { + AsyncImage( + model = getImageRequestBuilderForCytoid(level.bundle.backgroundImage.thumbnail) + .build(), + modifier = Modifier.fillMaxWidth(), + contentDescription = level.title, + onSuccess = { + try { + cacheBackgroundImageFile.createNewFile() + val output = + FileOutputStream( + cacheBackgroundImageFile ) - } - .onSuccess { - Looper.prepare() - "已保存至图库".showToast() - } - .onFailure { e -> + it.result.drawable.toBitmap() + .compress( + Bitmap.CompressFormat.PNG, + 100, + output + ) + output.flush() + output.close() + } catch (e: Exception) { e.printStackTrace() - context.runOnUiThread { - e - .stackTraceToString() - .showDialog( - context, - context.getString(R.string.fail) - ) { - this.setPositiveButton( - context.getString( - R.string.confirm - ) - ) { dialogInterface, _ -> - dialogInterface.dismiss() - } - } - } + e.stackTraceToString().showToast() + Crashes.trackError(e) } + }, + onError = { + backgroundImageIsError = true + }, + contentScale = ContentScale.FillWidth, + ) + AnimatedVisibility( + visible = backgroundImageIsError, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text( + text = stringResource(id = R.string.imageError), + fontWeight = FontWeight.SemiBold, + fontSize = LocalTextStyle.current.fontSize.times( + 2 + ) + ) } } - - 3 -> { - context - .getString(R.string.saving) - .showToast() - captureController.capture() - } } } - .create() - .show() + } } - ) - } - ) { - Column( - Modifier.padding(6.dp) - ) { - if (File( - externalCacheStorageDir, - "backgroundImage_${record.chart.level.uid}" - ).exists() && File( - externalCacheStorageDir, - "backgroundImage_${record.chart.level.uid}" - ).isFile - ) { - val input = FileInputStream( - File( - externalCacheStorageDir, - "backgroundImage_${record.chart.level.uid}" - ) - ) - val bitmap = BitmapFactory.decodeStream(input) - Card { - Image( - painter = BitmapPainter(bitmap.asImageBitmap()), - contentDescription = record.chart.level.title, - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth() - ) - } - } else { - Card { - Box { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) + } else { + Card { + Image( + painter = painterResource(id = R.drawable.sayakacry), + contentDescription = "LevelTitle", + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() ) - var backgroundImageIsError by remember { mutableStateOf(false) } - Column { - AsyncImage( - model = ImageRequest.Builder(context) - .data(record.chart.level.bundle.backgroundImage.thumbnail) - .crossfade(true) - .setHeader( - "User-Agent", - "CytoidClient/2.1.1" - ) - .crossfade(true) - .error(R.drawable.sayakacry) - .build(), - modifier = Modifier.fillMaxWidth(), - contentDescription = record.chart.level.title, - onSuccess = { - val imageFile = File( - externalCacheStorageDir, - "backgroundImage_${record.chart.level.uid}" - ) - try { - imageFile.createNewFile() - val output = - FileOutputStream(imageFile) - it.result.drawable.toBitmap() - .compress( - Bitmap.CompressFormat.PNG, - 100, - output - ) - output.flush() - output.close() - } catch (e: Exception) { - e.printStackTrace() - e.stackTraceToString().showToast() - Crashes.trackError(e) - } - }, - onError = { - backgroundImageIsError = true - }, - contentScale = ContentScale.FillWidth, - ) - AnimatedVisibility( - visible = backgroundImageIsError, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Text( - text = stringResource(id = R.string.imageError), - fontWeight = FontWeight.SemiBold, - fontSize = LocalTextStyle.current.fontSize.times(2) - ) + } + } + record.chart?.level?.let { level -> + IconButton( + onClick = { + if (mediaPlayerState != Player.STATE_READY) { + val dataSourceFactory = + DefaultHttpDataSource.Factory() + .setDefaultRequestProperties(mapOf("User-Agent" to "CytoidClient/2.1.1")) + val mediaItem = MediaItem.Builder() + .setUri( + Uri.parse( + level.bundle.musicPreview ?: level.bundle.music + ) + ).build() + val internetAudioSource = + ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(mediaItem) + exoPlayer.setMediaSource(internetAudioSource) + exoPlayer.prepare() + exoPlayer.play() + } else { + exoPlayer.stop() } - } + }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .background( + MaterialTheme.colorScheme.primaryContainer, + CircleShape + ) + ) { + if (mediaPlayerState == Player.STATE_BUFFERING) CircularProgressIndicator( + Modifier.padding(6.dp) + ) + else Icon( + imageVector = if (mediaPlayerState == Player.STATE_READY) Icons.Default.Stop else Icons.Default.PlayArrow, + contentDescription = "${if (mediaPlayerState == Player.STATE_READY) "停止" else "播放"}音乐预览" + ) } } } @@ -365,52 +278,49 @@ fun RecordCard(record: UserRecord, recordIndex: Int? = null, keep2DecimalPlaces: Text(text = "#${it}.") } Text( - text = record.chart.level.title, + text = record.chart?.level?.title ?: "LevelTitle", fontWeight = FontWeight.SemiBold, fontSize = LocalTextStyle.current.fontSize * 2, lineHeight = LocalTextStyle.current.lineHeight * 1.5 ) - Text(text = record.chart.level.uid) + Text(text = record.chart?.level?.uid ?: "LevelUid") Spacer(modifier = Modifier.height(6.dp)) - Text( - text = " ${ - record.chart.name - ?: record.chart.type.replaceFirstChar { - if (it.isLowerCase()) it.titlecase( - Locale.getDefault() - ) else it.toString() - } - } ${record.chart.difficulty} ", - color = Color.White, - modifier = Modifier - .background( - Brush.linearGradient( - when (record.chart.type) { - "easy" -> listOf( - Color(0xff4ca2cd), - Color(0xff67b26f) - ) - - "hard" -> listOf( - Color(0xff4568dc), - Color(0xffb06abc) - ) + record.chart?.let { chart -> + Text( + text = " ${ + chart.name + ?: chart.type.replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.getDefault() + ) else it.toString() + } + } ${chart.difficulty} ", + color = Color.White, + modifier = Modifier + .background( + Brush.linearGradient( + when (chart.type) { + "easy" -> listOf( + Color(0xff4ca2cd), + Color(0xff67b26f) + ) - "extreme" -> listOf( - Color(0xFF200122), - Color(0xff6f0000) + "extreme" -> listOf( + Color(0xFF200122), + Color(0xff6f0000) - ) + ) - else -> listOf( - Color(0xff4568dc), - Color(0xffb06abc) - ) - } - ), RoundedCornerShape(CornerSize(100)) - ) - .padding(6.dp) - ) + else -> listOf( + Color(0xff4568dc), + Color(0xffb06abc) + ) + } + ), RoundedCornerShape(CornerSize(100)) + ) + .padding(6.dp) + ) + } Text( text = record.score.toString(), fontSize = LocalTextStyle.current.fontSize.times(3) @@ -423,17 +333,17 @@ fun RecordCard(record: UserRecord, recordIndex: Int? = null, keep2DecimalPlaces: Image( painter = painterResource( id = when (mod) { - "HideNotes" -> R.drawable.hide_notes - "HideScanline" -> R.drawable.hide_scanline - "Slow" -> R.drawable.slow - "Fast" -> R.drawable.fast - "Hard" -> R.drawable.hyper - "ExHard" -> R.drawable.another - "AP" -> R.drawable.ap - "FC" -> R.drawable.fc - "FlipAll" -> R.drawable.flip_all - "FlipX" -> R.drawable.flip_x - "FlipY" -> R.drawable.flip_y + "HideNotes" -> R.drawable.mod_hide_notes + "HideScanline" -> R.drawable.mod_hide_scanline + "Slow" -> R.drawable.mod_slow + "Fast" -> R.drawable.mod_fast + "Hard" -> R.drawable.mod_hyper + "ExHard" -> R.drawable.mod_another + "AP" -> R.drawable.mod_ap + "FC" -> R.drawable.mod_fc + "FlipAll" -> R.drawable.mod_flip_all + "FlipX" -> R.drawable.mod_flip_x + "FlipY" -> R.drawable.mod_flip_y else -> throw Exception("Unknown condition branch enter action") } ), @@ -466,39 +376,31 @@ fun RecordCard(record: UserRecord, recordIndex: Int? = null, keep2DecimalPlaces: } ", color = Color.White ) - Row { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { Text(text = "Perfect") - Spacer(modifier = Modifier.width(6.dp)) Text( text = record.details.perfect.toString(), color = Color(0xff60a5fa) ) - } - Row { Text(text = "Great") - Spacer(modifier = Modifier.width(6.dp)) Text( text = record.details.great.toString(), color = Color(0xfffacc15) ) - Spacer(modifier = Modifier.width(12.dp)) Text(text = "Good") - Spacer(modifier = Modifier.width(6.dp)) Text( text = record.details.good.toString(), color = Color(0xff4ade80) ) - } - Row { Text(text = "Bad") - Spacer(modifier = Modifier.width(6.dp)) Text( text = record.details.bad.toString(), color = Color(0xfff87171) ) - Spacer(modifier = Modifier.width(12.dp)) Text(text = "Miss") - Spacer(modifier = Modifier.width(6.dp)) Text( text = record.details.miss.toString(), color = Color(0xff94a3b8) @@ -510,5 +412,254 @@ fun RecordCard(record: UserRecord, recordIndex: Int? = null, keep2DecimalPlaces: ) } } + if (recordDialogState) AlertDialog( + onDismissRequest = { recordDialogState = false }, + confirmButton = {}, + title = { + Text( + text = record.chart?.level?.title ?: "LevelTitle", + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Column( + Modifier.verticalScroll(rememberScrollState()) + ) { + ListItem( + headlineContent = { Text(context.getString(R.string.view_in_cytoid)) }, + modifier = Modifier.clickable { + if (record.chart?.level?.uid == null) { + "谱面信息缺失,无法查看!".showToast() + } else { + if (BaseApplication.cytoidIsInstalled) { + BaseApplication.context.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse(CytoidDeepLink.getCytoidLevelDeepLink(record.chart.level.uid)) + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } else context + .getString(R.string.cytoid_is_not_installed) + .showToast() + } + } + ) + ListItem( + headlineContent = { Text(text = stringResource(id = R.string.view_in_cytoidIO)) }, + modifier = Modifier.clickable { + if (record.chart?.level?.uid == null) { + "谱面信息缺失,无法查看!".showToast() + } else { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://cytoid.io/levels/${record.chart.level.uid}") + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + BaseApplication.context.startActivity(intent) + } + } + ) + ListItem( + headlineContent = { Text(context.getString(R.string.copy_content)) }, + modifier = Modifier.clickable { + copyRecordContentDialogState = true + } + ) + ListItem( + headlineContent = { Text(context.getString(R.string.save_illustration)) }, + modifier = Modifier.clickable { + if (record.chart?.level?.uid == null) { + "谱面信息缺失,无法保存!".showToast() + } else { + context + .getString(R.string.saving_illustration) + .showToast() + thread { + kotlin + .runCatching { + URL(record.chart.level.bundle.backgroundImage.original) + .toBitmap() + .saveIntoMediaStore( + context.contentResolver, + ContentValues() + ) + } + .onSuccess { + Looper.prepare() + context + .getString(R.string.saved_into_gallery) + .showToast() + } + .onFailure { e -> + e.printStackTrace() + context.runOnUiThread { + e + .stackTraceToString() + .showDialog( + context, + context.getString(R.string.fail) + ) + } + } + } + } + } + ) + ListItem( + headlineContent = { Text(context.getString(R.string.save_as_picture)) }, + modifier = Modifier.clickable { + context + .getString(R.string.saving) + .showToast() + captureController.capture() + + } + ) + } + } + ) + if (copyRecordContentDialogState) AlertDialog( + onDismissRequest = { copyRecordContentDialogState = false }, + confirmButton = {}, + title = { + Text( + text = context.resources.getString(R.string.choose_copy_content), + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Column( + Modifier.verticalScroll(rememberScrollState()) + ) { + if (record.chart?.level != null) { + ListItem( + headlineContent = { Text(record.chart.level.title) }, + modifier = Modifier.clickable { + record.chart.level.title.saveIntoClipboard() + } + ) + ListItem( + headlineContent = { Text(record.chart.level.uid) }, + modifier = Modifier.clickable { + record.chart.level.uid.saveIntoClipboard() + } + ) + ListItem( + headlineContent = { + Text("${ + record.chart.name + ?: record.chart.type.replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.getDefault() + ) else it.toString() + } + } ${record.chart.difficulty}") + }, + modifier = Modifier.clickable { + "${ + record.chart.name + ?: record.chart.type.replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.getDefault() + ) else it.toString() + } + } ${record.chart.difficulty}".saveIntoClipboard() + } + ) + } + ListItem( + headlineContent = { Text(record.score.toString()) }, + modifier = Modifier.clickable { + record.score + .toString() + .saveIntoClipboard() + } + ) + ListItem( + headlineContent = { Text("Mods:${record.mods}") }, + modifier = Modifier.clickable { + "Mods:${record.mods}".saveIntoClipboard() + } + ) + ListItem( + headlineContent = { + Text("${ + (record.accuracy * 100).run { + if (keep2DecimalPlaces) this.setPrecision( + 2 + ) else this + } + }% accuracy ${record.details.maxCombo} max combo") + }, + modifier = Modifier.clickable { + "${ + (record.accuracy * 100).run { + if (keep2DecimalPlaces) this.setPrecision( + 2 + ) else this + } + }% accuracy ${record.details.maxCombo} max combo".saveIntoClipboard() + } + ) + ListItem( + headlineContent = { + Text("Rating ${ + record.rating.run { + if (keep2DecimalPlaces) this.setPrecision( + 2 + ) else this + } + }") + }, + modifier = Modifier.clickable { + "Rating ${ + record.rating.run { + if (keep2DecimalPlaces) this.setPrecision( + 2 + ) else this + } + }".saveIntoClipboard() + } + ) + ListItem( + headlineContent = { Text("Perfect ${record.details.perfect} Great ${record.details.great} Good ${record.details.good} Bad ${record.details.bad} Miss ${record.details.miss}") }, + modifier = Modifier.clickable { + "Perfect ${record.details.perfect} Great ${record.details.great} Good ${record.details.good} Bad ${record.details.bad} Miss ${record.details.miss}".saveIntoClipboard() + } + ) + ListItem( + headlineContent = { + Text( + DateParser + .parseISO8601Date(record.date) + .formatToTimeString() + ) + }, + modifier = Modifier.clickable { + DateParser + .parseISO8601Date(record.date) + .formatToTimeString() + .saveIntoClipboard() + + } + ) + ListItem( + headlineContent = { Text(context.getString(R.string.all_contents)) }, + modifier = Modifier.clickable { + record + .detailsString() + .saveIntoClipboard() + } + ) + ListItem( + headlineContent = { Text("(仅调试)UserRecord对象") }, + modifier = Modifier.clickable { + record + .toString() + .saveIntoClipboard() + } + ) + } + } + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/TopBar.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/TopBar.kt index 6ad3239..9272593 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/TopBar.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/ui/compose/component/TopBar.kt @@ -4,6 +4,7 @@ import android.os.Looper import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Public import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -17,12 +18,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import com.lyneon.cytoidinfoquerier.BaseApplication import com.lyneon.cytoidinfoquerier.R -import com.lyneon.cytoidinfoquerier.tool.extension.showToast +import com.lyneon.cytoidinfoquerier.util.extension.showToast import kotlinx.coroutines.launch import okhttp3.OkHttpClient import okhttp3.Request @@ -30,7 +29,10 @@ import kotlin.concurrent.thread @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TopBar(title: String = BaseApplication.context.getString(R.string.app_name)) { +fun TopBar( + title: String = BaseApplication.context.getString(R.string.app_name), + additionalActions: @Composable ((Unit) -> Unit)? = null +) { val scope = rememberCoroutineScope() CenterAlignedTopAppBar( title = { Text(text = title) }, @@ -48,9 +50,10 @@ fun TopBar(title: String = BaseApplication.context.getString(R.string.app_name)) }, actions = { var menuIsExpanded by remember { mutableStateOf(false) } + additionalActions?.invoke(Unit) IconButton(onClick = { menuIsExpanded = !menuIsExpanded }) { Icon( - imageVector = Icons.Filled.MoreVert, + imageVector = Icons.Default.MoreVert, contentDescription = "选项菜单" ) } @@ -60,7 +63,7 @@ fun TopBar(title: String = BaseApplication.context.getString(R.string.app_name)) DropdownMenuItem( leadingIcon = { Icon( - imageVector = ImageVector.vectorResource(R.drawable.baseline_public_24), + imageVector = Icons.Default.Public, contentDescription = stringResource(id = R.string.ping) ) }, diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/FileTool.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/FileUtil.kt similarity index 61% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/tool/FileTool.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/util/FileUtil.kt index 505052d..84aefa7 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/FileTool.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/FileUtil.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.tool +package com.lyneon.cytoidinfoquerier.util import android.content.Context import android.graphics.Bitmap @@ -8,15 +8,16 @@ import java.io.BufferedWriter import java.io.InputStreamReader import java.io.OutputStreamWriter -fun Context.saveStringFile(fileName: String, fileContent: String) { +fun Context.writeStringIntoFile(fileName: String, fileContent: String) { val outputStream = this.openFileOutput(fileName, Context.MODE_PRIVATE) val writer = BufferedWriter(OutputStreamWriter(outputStream)) writer.use { it.write(fileContent) } + outputStream.close() } -fun Context.loadStringFile(fileName: String): String { +fun Context.readStringFromFile(fileName: String): String { val fileContent = StringBuilder() val inputStream = this.openFileInput(fileName) val reader = BufferedReader(InputStreamReader(inputStream)) @@ -25,20 +26,26 @@ fun Context.loadStringFile(fileName: String): String { fileContent.append(it) } } + inputStream.close() return fileContent.toString() } -fun Context.saveImageFile(fileName: String, image: Bitmap) { +fun Context.writeImageIntoFile(fileName: String, image: Bitmap) { + val output = this.openFileOutput(fileName, Context.MODE_PRIVATE) try { - val output = this.openFileOutput(fileName, Context.MODE_PRIVATE) image.compress(Bitmap.CompressFormat.JPEG, 100, output) output.flush() - output.close() } catch (e: Exception) { e.printStackTrace() throw e + } finally { + output.close() } } -fun Context.loadImageFile(fileName: String): Bitmap = - BitmapFactory.decodeStream(this.openFileInput(fileName)) \ No newline at end of file +fun Context.readImageFromFile(fileName: String): Bitmap { + val inputStream = this.openFileInput(fileName) + val image = BitmapFactory.decodeStream(inputStream) + inputStream.close() + return image +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/util/LayoutBitmap.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/LayoutBitmap.kt new file mode 100644 index 0000000..0e10562 --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/LayoutBitmap.kt @@ -0,0 +1,196 @@ +package com.lyneon.cytoidinfoquerier.util + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import com.lyneon.cytoidinfoquerier.util.extension.enableAntiAlias +import com.patrykandpatrick.vico.core.extension.ceil +import com.patrykandpatrick.vico.core.extension.lineHeight +import kotlin.math.abs + +/** + * 按照传入的组件自动进行线性布局绘制的位图 + * + * 在调用getBitmap()之前,不会创建位图对象进行任何实际的绘制操作 + * */ +sealed interface LayoutBitmap { + val componentsList: MutableList + fun getBitmap(): Bitmap + + fun drawComponent(component: LayoutBitmapComponent, x: Float, y: Float, canvas: Canvas) { + when (component) { + is TextComponent -> canvas.drawText( + component.text, + x, + y + abs(component.paint.ascent()), + component.paint + ) + + is ImageComponent -> canvas.drawBitmap(component.bitmap, x, y, null) + is SpaceComponent -> {} + is RectComponent -> canvas.drawRect( + x, + y, + x + component.width.toFloat(), + component.height.toFloat(), + component.paint + ) + + is RoundRectComponent -> canvas.drawRoundRect( + x, + y, + x + component.width.toFloat(), + component.height.toFloat(), + component.rx, + component.ry, + component.paint + ) + + is BackgroundColorComponent -> canvas.drawARGB( + component.a, + component.r, + component.g, + component.b + ) + } + } + + fun addText(text: String, paint: Paint) { + componentsList.add(TextComponent(text, paint)) + } + + fun addText(textComponent: TextComponent) { + componentsList.add(textComponent) + } + + fun addBitmap(bitmap: Bitmap) { + componentsList.add(ImageComponent(bitmap)) + } + + fun addBitmap(imageComponent: ImageComponent) { + componentsList.add(imageComponent) + } + + fun addSpace(size: Int) { + componentsList.add(SpaceComponent(size)) + } + + fun addSpace(spaceComponent: SpaceComponent) { + componentsList.add(spaceComponent) + } + + fun addRect(width: Int, height: Int, paint: Paint) { + componentsList.add(RectComponent(width, height, paint)) + } + + fun addRect(rectComponent: RoundRectComponent) { + componentsList.add(rectComponent) + } + + fun addRoundRect(w: Int, h: Int, rx: Float, ry: Float, paint: Paint) { + componentsList.add(RoundRectComponent(w, h, rx, ry, paint)) + } + + fun addRoundRect(roundRectComponent: RoundRectComponent) { + componentsList.add(roundRectComponent) + } + + fun setBackgroundColor(a: Int, r: Int, g: Int, b: Int) { + componentsList.add(BackgroundColorComponent(a, r, g, b)) + } +} + +sealed interface LayoutBitmapComponent { + val width: Int + val height: Int +} + +class TextComponent(val text: String, val paint: Paint) : + LayoutBitmapComponent { + override val width: Int + get() = paint.measureText(text).ceil.toInt() + override val height: Int + get() = paint.lineHeight.ceil.toInt() +} + +class ImageComponent(val bitmap: Bitmap) : LayoutBitmapComponent { + override val width: Int get() = bitmap.width + override val height: Int get() = bitmap.height +} + +class SpaceComponent(val size: Int) : LayoutBitmapComponent { + override val width: Int get() = size + override val height: Int get() = size +} + +class RectComponent(val w: Int, val h: Int, val paint: Paint) : + LayoutBitmapComponent { + override val width: Int get() = w + override val height: Int get() = h +} + +class RoundRectComponent( + val w: Int, val h: Int, val rx: Float, val ry: Float, val paint: Paint +) : LayoutBitmapComponent { + override val width: Int get() = w + override val height: Int get() = h +} + +class BackgroundColorComponent(val a: Int, val r: Int, val g: Int, val b: Int) : + LayoutBitmapComponent { + override val width: Int get() = 0 + override val height: Int get() = 0 +} + +class RowBitmap( + val padding: Int? = null, + val contentSpacing: Int? = null +) : LayoutBitmap { + override val componentsList: MutableList = mutableListOf() + + override fun getBitmap(): Bitmap { + val width = componentsList.sumOf { it.width } + 2 * (padding ?: 0) + + (contentSpacing?.times((componentsList.size - 1)) ?: 0) + val height = componentsList.maxOf { it.height } + 2 * (padding ?: 0) + + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap).apply { enableAntiAlias() } + var x = 0f + padding?.let { x += it } + componentsList.forEach { + drawComponent(it, x, 0f + (padding?.toFloat() ?: 0f), canvas) + x += it.width + if (contentSpacing != null) { + drawComponent(SpaceComponent(contentSpacing), x, 0f, canvas) + x += contentSpacing + } + } + return bitmap + } +} + +class ColumnBitmap( + val padding: Int? = null, + val contentSpacing: Int? = null +) : LayoutBitmap { + override val componentsList: MutableList = mutableListOf() + + override fun getBitmap(): Bitmap { + val width = componentsList.maxOf { it.width } + 2 * (padding ?: 0) + val height = componentsList.sumOf { it.height } + 2 * (padding ?: 0) + + (contentSpacing?.times((componentsList.size - 1)) ?: 0) + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap).apply { enableAntiAlias() } + var y = 0f + padding?.let { y += it } + componentsList.forEach { + drawComponent(it, 0f + (padding?.toFloat() ?: 0f), y, canvas) + y += it.height + if (contentSpacing != null) { + drawComponent(SpaceComponent(contentSpacing), 0f, y, canvas) + y += contentSpacing + } + } + return bitmap + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Bitmap.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Bitmap.kt similarity index 95% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Bitmap.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Bitmap.kt index 9d240e1..ce6f33b 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Bitmap.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Bitmap.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.tool.extension +package com.lyneon.cytoidinfoquerier.util.extension import android.content.ContentResolver import android.content.ContentValues diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Canvas.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Canvas.kt similarity index 82% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Canvas.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Canvas.kt index e488a21..9aaa304 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Canvas.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Canvas.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.tool.extension +package com.lyneon.cytoidinfoquerier.util.extension import android.graphics.Canvas import android.graphics.Paint diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Context.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Context.kt similarity index 81% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Context.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Context.kt index d130887..7c9151b 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Context.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Context.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.tool.extension +package com.lyneon.cytoidinfoquerier.util.extension import android.content.Context import android.content.Intent diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/ImageRequest.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/ImageRequest.kt new file mode 100644 index 0000000..b4a436b --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/ImageRequest.kt @@ -0,0 +1,12 @@ +package com.lyneon.cytoidinfoquerier.util.extension + +import coil.request.ImageRequest +import com.lyneon.cytoidinfoquerier.BaseApplication +import com.lyneon.cytoidinfoquerier.R + +fun getImageRequestBuilderForCytoid(data: Any) = + ImageRequest.Builder(BaseApplication.context) + .data(data) + .setHeader("User-Agent", "CytoidClient/2.1.1") + .crossfade(true) + .error(R.drawable.sayakacry) \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Int.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Int.kt new file mode 100644 index 0000000..837c4ee --- /dev/null +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Int.kt @@ -0,0 +1,23 @@ +package com.lyneon.cytoidinfoquerier.util.extension + +import com.lyneon.cytoidinfoquerier.data.constant.CytoidScoreRange + +fun Int.isMaxCytoidGrade() = this == CytoidScoreRange.max + +fun Int.isSSSCytoidGrade() = this in CytoidScoreRange.sss + +fun Int.isSSCytoidGrade() = this in CytoidScoreRange.ss + +fun Int.isSCytoidGrade() = this in CytoidScoreRange.s + +fun Int.isAACytoidGrade() = this in CytoidScoreRange.aa + +fun Int.isACytoidGrade() = this in CytoidScoreRange.a + +fun Int.isBCytoidGrade() = this in CytoidScoreRange.b + +fun Int.isCCytoidGrade() = this in CytoidScoreRange.c + +fun Int.isDCytoidGrade() = this in CytoidScoreRange.d + +fun Int.isFCytoidGrade() = this in CytoidScoreRange.f \ No newline at end of file diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Number.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Number.kt similarity index 84% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Number.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Number.kt index 35e1d6d..d89e8ce 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/Number.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/Number.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.tool.extension +package com.lyneon.cytoidinfoquerier.util.extension import java.math.RoundingMode import java.text.DecimalFormat diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/String.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/String.kt similarity index 95% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/String.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/String.kt index 4abad6b..7976bdc 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/String.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/String.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.tool.extension +package com.lyneon.cytoidinfoquerier.util.extension import android.app.Activity import android.app.AlertDialog diff --git a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/URL.kt b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/URL.kt similarity index 88% rename from app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/URL.kt rename to app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/URL.kt index 65159f7..cc0e2ff 100644 --- a/app/src/main/java/com/lyneon/cytoidinfoquerier/tool/extension/URL.kt +++ b/app/src/main/java/com/lyneon/cytoidinfoquerier/util/extension/URL.kt @@ -1,4 +1,4 @@ -package com.lyneon.cytoidinfoquerier.tool.extension +package com.lyneon.cytoidinfoquerier.util.extension import android.graphics.BitmapFactory import okhttp3.OkHttpClient diff --git a/app/src/main/res/drawable/baseline_bug_report_24.xml b/app/src/main/res/drawable/baseline_bug_report_24.xml deleted file mode 100644 index c72d5c2..0000000 --- a/app/src/main/res/drawable/baseline_bug_report_24.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_insights_24.xml b/app/src/main/res/drawable/baseline_insights_24.xml deleted file mode 100644 index 4b23dc5..0000000 --- a/app/src/main/res/drawable/baseline_insights_24.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/baseline_public_24.xml b/app/src/main/res/drawable/baseline_public_24.xml deleted file mode 100644 index dce682a..0000000 --- a/app/src/main/res/drawable/baseline_public_24.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_save_24.xml b/app/src/main/res/drawable/baseline_save_24.xml deleted file mode 100644 index 794184a..0000000 --- a/app/src/main/res/drawable/baseline_save_24.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/baseline_search_24.xml b/app/src/main/res/drawable/baseline_search_24.xml deleted file mode 100644 index 3cac090..0000000 --- a/app/src/main/res/drawable/baseline_search_24.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_app_center.xml b/app/src/main/res/drawable/ic_app_center.xml new file mode 100644 index 0000000..cffcdf6 --- /dev/null +++ b/app/src/main/res/drawable/ic_app_center.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_menu_copy.xml b/app/src/main/res/drawable/ic_menu_copy.xml deleted file mode 100644 index 15e9506..0000000 --- a/app/src/main/res/drawable/ic_menu_copy.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_menu_restart.xml b/app/src/main/res/drawable/ic_menu_restart.xml deleted file mode 100644 index 8b4f12b..0000000 --- a/app/src/main/res/drawable/ic_menu_restart.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ANOTHER.png b/app/src/main/res/drawable/mod_another.png similarity index 100% rename from app/src/main/res/drawable/ANOTHER.png rename to app/src/main/res/drawable/mod_another.png diff --git a/app/src/main/res/drawable/AP.png b/app/src/main/res/drawable/mod_ap.png similarity index 100% rename from app/src/main/res/drawable/AP.png rename to app/src/main/res/drawable/mod_ap.png diff --git a/app/src/main/res/drawable/Fast.png b/app/src/main/res/drawable/mod_fast.png similarity index 100% rename from app/src/main/res/drawable/Fast.png rename to app/src/main/res/drawable/mod_fast.png diff --git a/app/src/main/res/drawable/FC.png b/app/src/main/res/drawable/mod_fc.png similarity index 100% rename from app/src/main/res/drawable/FC.png rename to app/src/main/res/drawable/mod_fc.png diff --git a/app/src/main/res/drawable/flip_all.png b/app/src/main/res/drawable/mod_flip_all.png similarity index 100% rename from app/src/main/res/drawable/flip_all.png rename to app/src/main/res/drawable/mod_flip_all.png diff --git a/app/src/main/res/drawable/flip_x.png b/app/src/main/res/drawable/mod_flip_x.png similarity index 100% rename from app/src/main/res/drawable/flip_x.png rename to app/src/main/res/drawable/mod_flip_x.png diff --git a/app/src/main/res/drawable/flip_y.png b/app/src/main/res/drawable/mod_flip_y.png similarity index 100% rename from app/src/main/res/drawable/flip_y.png rename to app/src/main/res/drawable/mod_flip_y.png diff --git a/app/src/main/res/drawable/hide_notes.png b/app/src/main/res/drawable/mod_hide_notes.png similarity index 100% rename from app/src/main/res/drawable/hide_notes.png rename to app/src/main/res/drawable/mod_hide_notes.png diff --git a/app/src/main/res/drawable/hide_scanline.png b/app/src/main/res/drawable/mod_hide_scanline.png similarity index 100% rename from app/src/main/res/drawable/hide_scanline.png rename to app/src/main/res/drawable/mod_hide_scanline.png diff --git a/app/src/main/res/drawable/HYPER.png b/app/src/main/res/drawable/mod_hyper.png similarity index 100% rename from app/src/main/res/drawable/HYPER.png rename to app/src/main/res/drawable/mod_hyper.png diff --git a/app/src/main/res/drawable/Slow.png b/app/src/main/res/drawable/mod_slow.png similarity index 100% rename from app/src/main/res/drawable/Slow.png rename to app/src/main/res/drawable/mod_slow.png diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9fb6a95..daa802c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,14 +28,14 @@ 打开抽屉菜单 查询设置 RecentRecords - 数据分析 + Analytics 查询数量 查询数量不能为空 在 Cytoid 中查看 复制内容 选择要复制的内容 忽略已缓存数据 - 在 CytoidIO 中查看 + 在 cytoid.io 中查看 已安装的Cytoid信息 本应用使用Microsoft App Center进行错误信息收集,收集的数据中不会包含用户的个人信息,收集到的错误信息将仅用于修复漏洞。如果您不希望被收集这些数据,您可以选择在设置界面中关闭App Center数据收集,或者退出本应用 启用App Center功能 @@ -55,4 +55,21 @@ 查询结果为空 列数 列数不能为空 + 谱面管理 + 前往授权 + 取消 + 你好像没有安装Cytoid + 已保存至图库 + 个人简介 + 徽章 + 没有找到浏览器 + 展开 + 折叠 + 网格列数(竖屏) + 网格列数(横屏) + 网格列数 + 重置 + 旋转 + 减少 + 增加 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5379d46..510fe97 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.2.0" apply false + id("com.android.application") version "8.3.1" apply false id("org.jetbrains.kotlin.android") version "1.8.10" apply false kotlin("jvm") version "1.8.10" kotlin("plugin.serialization") version "1.8.10" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0440067..89ab4f6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Aug 26 22:20:38 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists