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