unofficial maimai DX NET client built on ATProto
0

Configure Feed

Select the types of activity you want to include in your feed.

m3e migration!!!

+2356 -1399
+125
androidApp/build.gradle.kts
··· 1 + import com.android.build.api.dsl.ApplicationExtension 2 + import org.gradle.api.file.RegularFileProperty 3 + import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 + import java.util.Properties 5 + 6 + plugins { 7 + alias(libs.plugins.android.application) 8 + alias(libs.plugins.compose.multiplatform) 9 + alias(libs.plugins.compose.compiler) 10 + id("io.github.tarasovvp.kmp-secrets-plugin") version "1.3.0" 11 + } 12 + 13 + val localProperties = 14 + Properties().apply { 15 + val file = rootProject.file("local.properties") 16 + if (file.isFile) { 17 + file.inputStream().use(::load) 18 + } 19 + } 20 + 21 + fun signingProperty(name: String): String? = 22 + providers.gradleProperty(name).orNull 23 + ?: providers.environmentVariable(name).orNull 24 + ?: localProperties.getProperty(name) 25 + 26 + val derakkumaKeystoreFile = rootProject.file("derakkuma.keystore") 27 + val derakkumaKeystorePassword = signingProperty("KEYSTORE_PASSWORD") 28 + val derakkumaKeyAlias = signingProperty("KEY_ALIAS") ?: "derakkuma" 29 + val derakkumaKeyPassword = signingProperty("KEY_PASSWORD") ?: derakkumaKeystorePassword 30 + val requestedReleaseBuild = 31 + gradle.startParameter.taskNames.any { taskName -> 32 + taskName.contains("Release", ignoreCase = true) 33 + } 34 + 35 + if (requestedReleaseBuild) { 36 + require(derakkumaKeystoreFile.isFile) { 37 + "Release builds must be signed with ${derakkumaKeystoreFile.path}" 38 + } 39 + require(!derakkumaKeystorePassword.isNullOrBlank()) { 40 + "Set KEYSTORE_PASSWORD in local.properties, Gradle properties, or the environment" 41 + } 42 + require(!derakkumaKeyPassword.isNullOrBlank()) { 43 + "Set KEY_PASSWORD in local.properties, Gradle properties, or the environment" 44 + } 45 + } 46 + 47 + kotlin { 48 + target { 49 + compilerOptions { 50 + jvmTarget.set(JvmTarget.JVM_17) 51 + freeCompilerArgs.add("-Xexpect-actual-classes") 52 + } 53 + } 54 + } 55 + 56 + dependencies { 57 + implementation(project(":composeApp")) 58 + implementation(libs.androidx.activity.compose) 59 + implementation(libs.posthog.kmp) 60 + } 61 + 62 + android { 63 + namespace = "com.derakkuma" 64 + compileSdk = 37 65 + 66 + signingConfigs { 67 + create("release") { 68 + storeFile = derakkumaKeystoreFile 69 + storePassword = derakkumaKeystorePassword.orEmpty() 70 + keyAlias = derakkumaKeyAlias 71 + keyPassword = derakkumaKeyPassword.orEmpty() 72 + } 73 + } 74 + 75 + defaultConfig { 76 + applicationId = "com.derakkuma" 77 + minSdk = 26 78 + targetSdk = 36 79 + versionCode = 2 80 + versionName = "0.1.1" 81 + } 82 + 83 + compileOptions { 84 + sourceCompatibility = JavaVersion.VERSION_17 85 + targetCompatibility = JavaVersion.VERSION_17 86 + } 87 + 88 + buildFeatures { 89 + compose = true 90 + } 91 + 92 + buildTypes { 93 + debug { 94 + applicationIdSuffix = ".debug" 95 + isDebuggable = true 96 + } 97 + release { 98 + signingConfig = signingConfigs.getByName("release") 99 + isMinifyEnabled = true 100 + isShrinkResources = false 101 + proguardFiles( 102 + getDefaultProguardFile("proguard-android-optimize.txt"), 103 + "../composeApp/proguard-rules.pro", 104 + ) 105 + } 106 + } 107 + } 108 + 109 + val emptyProguardMapFile = layout.buildDirectory.file("outputs/mapping/release/empty-proguard.map") 110 + val writeEmptyProguardMap = 111 + tasks.register("writeEmptyProguardMap") { 112 + outputs.file(emptyProguardMapFile) 113 + doLast { 114 + emptyProguardMapFile.get().asFile.writeText("") 115 + } 116 + } 117 + 118 + tasks.matching { it.name == "packageReleaseBundle" }.configureEach { 119 + dependsOn(writeEmptyProguardMap) 120 + val mappingFileProperty = 121 + javaClass.methods 122 + .firstOrNull { it.name == "getObsfuscationMappingFile" } 123 + ?.invoke(this) as? RegularFileProperty 124 + mappingFileProperty?.set(emptyProguardMapFile) 125 + }
+1
build.gradle.kts
··· 4 4 alias(libs.plugins.compose.multiplatform) apply false 5 5 alias(libs.plugins.compose.compiler) apply false 6 6 alias(libs.plugins.android.application) apply false 7 + alias(libs.plugins.android.multiplatform.library) apply false 7 8 }
+13 -110
composeApp/build.gradle.kts
··· 1 1 import org.gradle.api.tasks.Copy 2 2 import org.gradle.api.tasks.Delete 3 3 import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 - import org.gradle.api.file.RegularFileProperty 5 - import java.util.Properties 6 4 7 5 plugins { 8 6 alias(libs.plugins.kotlin.multiplatform) 9 7 alias(libs.plugins.kotlin.serialization) 10 8 alias(libs.plugins.compose.multiplatform) 11 9 alias(libs.plugins.compose.compiler) 12 - alias(libs.plugins.android.application) 10 + alias(libs.plugins.android.multiplatform.library) 13 11 alias(libs.plugins.sqldelight) 14 12 id("io.github.tarasovvp.kmp-secrets-plugin") version "1.3.0" 15 13 id("com.diffplug.spotless") version "8.2.1" 16 14 } 17 15 18 - val localProperties = 19 - Properties().apply { 20 - val file = rootProject.file("local.properties") 21 - if (file.isFile) { 22 - file.inputStream().use(::load) 23 - } 24 - } 25 - 26 - fun signingProperty(name: String): String? = 27 - providers.gradleProperty(name).orNull 28 - ?: providers.environmentVariable(name).orNull 29 - ?: localProperties.getProperty(name) 30 - 31 - val derakkumaKeystoreFile = rootProject.file("derakkuma.keystore") 32 - val derakkumaKeystorePassword = signingProperty("KEYSTORE_PASSWORD") 33 - val derakkumaKeyAlias = signingProperty("KEY_ALIAS") ?: "derakkuma" 34 - val derakkumaKeyPassword = signingProperty("KEY_PASSWORD") ?: derakkumaKeystorePassword 35 - val requestedReleaseBuild = 36 - gradle.startParameter.taskNames.any { taskName -> 37 - taskName.contains("Release", ignoreCase = true) 38 - } 39 - 40 - if (requestedReleaseBuild) { 41 - require(derakkumaKeystoreFile.isFile) { 42 - "Release builds must be signed with ${derakkumaKeystoreFile.path}" 43 - } 44 - require(!derakkumaKeystorePassword.isNullOrBlank()) { 45 - "Set KEYSTORE_PASSWORD in local.properties, Gradle properties, or the environment" 46 - } 47 - require(!derakkumaKeyPassword.isNullOrBlank()) { 48 - "Set KEY_PASSWORD in local.properties, Gradle properties, or the environment" 49 - } 50 - } 51 - 52 16 spotless { 53 17 kotlin { 54 18 target("**/*.kt") ··· 62 26 } 63 27 64 28 kotlin { 65 - androidTarget { 29 + androidLibrary { 30 + namespace = "com.derakkuma.library" 31 + compileSdk = 37 32 + minSdk = 26 33 + 34 + androidResources { 35 + enable = true 36 + } 37 + 66 38 compilerOptions { 67 39 jvmTarget.set(JvmTarget.JVM_17) 68 40 } ··· 89 61 commonMain.dependencies { 90 62 implementation(compose.runtime) 91 63 implementation(compose.foundation) 92 - implementation(compose.material3) 64 + implementation("org.jetbrains.compose.material3:material3:1.12.0-alpha01") 93 65 implementation(compose.ui) 94 66 implementation(compose.components.resources) 95 67 implementation("org.jetbrains.compose.material:material-icons-extended:1.7.3") ··· 120 92 } 121 93 122 94 androidMain.dependencies { 123 - implementation("androidx.browser:browser:1.8.0") 124 - implementation("androidx.core:core-ktx:1.17.0") 95 + implementation(libs.androidx.browser) 96 + implementation(libs.androidx.core.ktx) 125 97 implementation(libs.ktor.client.okhttp) 126 98 implementation(libs.sqldelight.android) 127 99 implementation(libs.androidx.activity.compose) ··· 129 101 implementation(libs.crypto.jdk) 130 102 } 131 103 132 - androidUnitTest.dependencies { 133 - implementation(kotlin("test")) 134 - } 135 - 136 104 iosMain.dependencies { 137 105 implementation(libs.ktor.client.darwin) 138 106 implementation(libs.sqldelight.native) ··· 141 109 } 142 110 } 143 111 144 - android { 145 - namespace = "com.derakkuma" 146 - compileSdk = 36 147 - 148 - signingConfigs { 149 - create("release") { 150 - storeFile = derakkumaKeystoreFile 151 - storePassword = derakkumaKeystorePassword.orEmpty() 152 - keyAlias = derakkumaKeyAlias 153 - keyPassword = derakkumaKeyPassword.orEmpty() 154 - } 155 - } 156 - 157 - defaultConfig { 158 - applicationId = "com.derakkuma" 159 - minSdk = 26 160 - targetSdk = 36 161 - versionCode = 2 162 - versionName = "0.1.1" 163 - } 164 - 165 - compileOptions { 166 - sourceCompatibility = JavaVersion.VERSION_17 167 - targetCompatibility = JavaVersion.VERSION_17 168 - } 169 - 170 - buildFeatures { 171 - compose = true 172 - } 173 - 174 - buildTypes { 175 - debug { 176 - applicationIdSuffix = ".debug" 177 - isDebuggable = true 178 - } 179 - release { 180 - signingConfig = signingConfigs.getByName("release") 181 - isMinifyEnabled = true 182 - isShrinkResources = false 183 - proguardFiles( 184 - getDefaultProguardFile("proguard-android-optimize.txt"), 185 - "proguard-rules.pro", 186 - ) 187 - } 188 - } 189 - } 190 - 191 112 sqldelight { 192 113 databases { 193 114 create("DerakkumaDb") { ··· 226 147 delete(staleAppIconPreviewResourceTree()) 227 148 } 228 149 229 - val emptyProguardMapFile = layout.buildDirectory.file("outputs/mapping/release/empty-proguard.map") 230 - val writeEmptyProguardMap = 231 - tasks.register("writeEmptyProguardMap") { 232 - outputs.file(emptyProguardMapFile) 233 - doLast { 234 - emptyProguardMapFile.get().asFile.writeText("") 235 - } 236 - } 237 - 238 150 tasks.configureEach { 239 151 val taskName = name 240 152 val copiesComposeResourcesToAndroid = taskName.matches(Regex("copy.*ComposeResourcesToAndroidAssets")) ··· 255 167 dependsOn(deleteStaleAppIconPreviewResources) 256 168 } 257 169 } 258 - 259 - tasks.matching { it.name == "packageReleaseBundle" }.configureEach { 260 - dependsOn(writeEmptyProguardMap) 261 - val mappingFileProperty = 262 - javaClass.methods 263 - .firstOrNull { it.name == "getObsfuscationMappingFile" } 264 - ?.invoke(this) as? RegularFileProperty 265 - mappingFileProperty?.set(emptyProguardMapFile) 266 - }
composeApp/src/androidMain/AndroidManifest.xml androidApp/src/main/AndroidManifest.xml
composeApp/src/androidMain/assets/app-icon-assets/aoharukkuma_fg.png androidApp/src/main/assets/app-icon-assets/aoharukkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/aoharukkuma_mono.png androidApp/src/main/assets/app-icon-assets/aoharukkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/bg.png androidApp/src/main/assets/app-icon-assets/bg.png
composeApp/src/androidMain/assets/app-icon-assets/default_fg.png androidApp/src/main/assets/app-icon-assets/default_fg.png
composeApp/src/androidMain/assets/app-icon-assets/default_mono.png androidApp/src/main/assets/app-icon-assets/default_mono.png
composeApp/src/androidMain/assets/app-icon-assets/doragokkuma_fg.png androidApp/src/main/assets/app-icon-assets/doragokkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/doragokkuma_mono.png androidApp/src/main/assets/app-icon-assets/doragokkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/enjekkuma_fg.png androidApp/src/main/assets/app-icon-assets/enjekkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/enjekkuma_mono.png androidApp/src/main/assets/app-icon-assets/enjekkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/higekkuma_fg.png androidApp/src/main/assets/app-icon-assets/higekkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/higekkuma_mono.png androidApp/src/main/assets/app-icon-assets/higekkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/kurobarakkuma_fg.png androidApp/src/main/assets/app-icon-assets/kurobarakkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/kurobarakkuma_mono.png androidApp/src/main/assets/app-icon-assets/kurobarakkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/lemonkkuma_fg.png androidApp/src/main/assets/app-icon-assets/lemonkkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/lemonkkuma_mono.png androidApp/src/main/assets/app-icon-assets/lemonkkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/limekkuma_fg.png androidApp/src/main/assets/app-icon-assets/limekkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/limekkuma_mono.png androidApp/src/main/assets/app-icon-assets/limekkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/meganekkuma_fg.png androidApp/src/main/assets/app-icon-assets/meganekkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/meganekkuma_mono.png androidApp/src/main/assets/app-icon-assets/meganekkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/mitsuyoshikkuma_fg.png androidApp/src/main/assets/app-icon-assets/mitsuyoshikkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/mitsuyoshikkuma_mono.png androidApp/src/main/assets/app-icon-assets/mitsuyoshikkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/ninjakkuma_fg.png androidApp/src/main/assets/app-icon-assets/ninjakkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/ninjakkuma_mono.png androidApp/src/main/assets/app-icon-assets/ninjakkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/onemukkuma_fg.png androidApp/src/main/assets/app-icon-assets/onemukkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/onemukkuma_mono.png androidApp/src/main/assets/app-icon-assets/onemukkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/pandakkuma_fg.png androidApp/src/main/assets/app-icon-assets/pandakkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/pandakkuma_mono.png androidApp/src/main/assets/app-icon-assets/pandakkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/risukkuma_fg.png androidApp/src/main/assets/app-icon-assets/risukkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/risukkuma_mono.png androidApp/src/main/assets/app-icon-assets/risukkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/tanukkuma_fg.png androidApp/src/main/assets/app-icon-assets/tanukkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/tanukkuma_mono.png androidApp/src/main/assets/app-icon-assets/tanukkuma_mono.png
composeApp/src/androidMain/assets/app-icon-assets/yetikkuma_fg.png androidApp/src/main/assets/app-icon-assets/yetikkuma_fg.png
composeApp/src/androidMain/assets/app-icon-assets/yetikkuma_mono.png androidApp/src/main/assets/app-icon-assets/yetikkuma_mono.png
composeApp/src/androidMain/kotlin/com/derakkuma/MainActivity.kt androidApp/src/main/kotlin/com/derakkuma/MainActivity.kt
+5 -6
composeApp/src/androidMain/kotlin/com/derakkuma/appicon/AppIcon.android.kt
··· 24 24 return Color(0xFFEADDFF) to Color(0xFF4F378B) 25 25 } 26 26 27 - actual suspend fun loadAppIconPreviewAsset(path: String): ImageBitmap? = 28 - runCatching { 29 - DerakkumaApp.context.assets.open("app-icon-assets/$path").use { stream -> 30 - stream.readBytes().decodeToImageBitmap() 31 - } 32 - }.getOrNull() 27 + actual suspend fun loadAppIconPreviewAsset(path: String): ImageBitmap? = runCatching { 28 + DerakkumaApp.context.assets.open("app-icon-assets/$path").use { stream -> 29 + stream.readBytes().decodeToImageBitmap() 30 + } 31 + }.getOrNull() 33 32 34 33 actual fun setAppIcon(option: AppIconOption): Boolean { 35 34 val context = DerakkumaApp.context
+1 -2
composeApp/src/androidMain/kotlin/com/derakkuma/ui/util/QrLogoImageRequest.android.kt
··· 3 3 import coil3.request.ImageRequest 4 4 import coil3.request.allowHardware 5 5 6 - actual fun ImageRequest.Builder.configureQrLogoRequest(): ImageRequest.Builder = 7 - allowHardware(false) 6 + actual fun ImageRequest.Builder.configureQrLogoRequest(): ImageRequest.Builder = allowHardware(false)
composeApp/src/androidMain/res/bg.png androidApp/src/main/res/bg.png
composeApp/src/androidMain/res/fg.png androidApp/src/main/res/fg.png
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_aoharukkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_aoharukkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_aoharukkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_aoharukkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_doragokkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_doragokkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_doragokkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_doragokkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_enjekkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_enjekkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_enjekkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_enjekkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_higekkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_higekkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_higekkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_higekkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_kurobarakkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_kurobarakkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_kurobarakkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_kurobarakkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_lemonkkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_lemonkkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_lemonkkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_lemonkkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_limekkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_limekkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_limekkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_limekkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_meganekkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_meganekkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_meganekkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_meganekkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_mitsuyoshikkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_mitsuyoshikkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_mitsuyoshikkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_mitsuyoshikkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_ninjakkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_ninjakkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_ninjakkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_ninjakkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_onemukkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_onemukkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_onemukkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_onemukkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_pandakkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_pandakkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_pandakkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_pandakkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_risukkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_risukkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_risukkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_risukkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_tanukkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_tanukkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_tanukkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_tanukkuma_round.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_yetikkuma.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_yetikkuma.xml
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_yetikkuma_round.xml androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_yetikkuma_round.xml
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_aoharukkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_aoharukkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_aoharukkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_aoharukkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_aoharukkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_aoharukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_aoharukkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_aoharukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_aoharukkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_aoharukkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_doragokkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_doragokkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_doragokkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_doragokkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_doragokkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_doragokkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_doragokkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_doragokkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_doragokkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_doragokkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_enjekkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_enjekkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_enjekkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_enjekkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_enjekkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_enjekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_enjekkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_enjekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_enjekkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_enjekkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_higekkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_higekkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_higekkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_higekkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_higekkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_higekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_higekkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_higekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_higekkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_higekkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_kurobarakkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_kurobarakkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_kurobarakkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_kurobarakkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_kurobarakkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_kurobarakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_kurobarakkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_kurobarakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_kurobarakkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_kurobarakkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_lemonkkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_lemonkkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_lemonkkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_lemonkkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_lemonkkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_lemonkkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_lemonkkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_lemonkkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_lemonkkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_lemonkkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_limekkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_limekkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_limekkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_limekkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_limekkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_limekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_limekkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_limekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_limekkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_limekkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_meganekkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_meganekkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_meganekkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_meganekkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_meganekkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_meganekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_meganekkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_meganekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_meganekkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_meganekkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_mitsuyoshikkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_ninjakkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_ninjakkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_ninjakkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_ninjakkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_ninjakkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_ninjakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_ninjakkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_ninjakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_ninjakkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_ninjakkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_onemukkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_onemukkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_onemukkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_onemukkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_onemukkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_onemukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_onemukkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_onemukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_onemukkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_onemukkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_pandakkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_pandakkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_pandakkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_pandakkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_pandakkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_pandakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_pandakkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_pandakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_pandakkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_pandakkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_risukkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_risukkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_risukkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_risukkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_risukkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_risukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_risukkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_risukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_risukkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_risukkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_tanukkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_tanukkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_tanukkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_tanukkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_tanukkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_tanukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_tanukkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_tanukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_tanukkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_tanukkuma_round.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_yetikkuma.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_yetikkuma.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_yetikkuma_background.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_yetikkuma_background.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_yetikkuma_foreground.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_yetikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_yetikkuma_monochrome.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_yetikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_yetikkuma_round.webp androidApp/src/main/res/mipmap-hdpi/ic_launcher_yetikkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_aoharukkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_aoharukkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_aoharukkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_aoharukkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_aoharukkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_aoharukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_aoharukkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_aoharukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_aoharukkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_aoharukkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_doragokkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_doragokkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_doragokkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_doragokkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_doragokkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_doragokkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_doragokkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_doragokkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_doragokkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_doragokkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_enjekkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_enjekkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_enjekkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_enjekkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_enjekkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_enjekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_enjekkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_enjekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_enjekkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_enjekkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_higekkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_higekkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_higekkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_higekkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_higekkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_higekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_higekkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_higekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_higekkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_higekkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_kurobarakkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_kurobarakkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_kurobarakkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_kurobarakkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_kurobarakkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_kurobarakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_kurobarakkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_kurobarakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_kurobarakkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_kurobarakkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_lemonkkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_lemonkkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_lemonkkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_lemonkkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_lemonkkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_lemonkkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_lemonkkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_lemonkkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_lemonkkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_lemonkkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_limekkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_limekkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_limekkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_limekkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_limekkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_limekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_limekkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_limekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_limekkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_limekkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_meganekkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_meganekkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_meganekkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_meganekkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_meganekkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_meganekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_meganekkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_meganekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_meganekkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_meganekkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_mitsuyoshikkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_ninjakkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_ninjakkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_ninjakkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_ninjakkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_ninjakkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_ninjakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_ninjakkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_ninjakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_ninjakkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_ninjakkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_onemukkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_onemukkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_onemukkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_onemukkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_onemukkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_onemukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_onemukkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_onemukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_onemukkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_onemukkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_pandakkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_pandakkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_pandakkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_pandakkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_pandakkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_pandakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_pandakkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_pandakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_pandakkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_pandakkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_risukkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_risukkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_risukkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_risukkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_risukkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_risukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_risukkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_risukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_risukkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_risukkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_tanukkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_tanukkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_tanukkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_tanukkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_tanukkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_tanukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_tanukkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_tanukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_tanukkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_tanukkuma_round.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_yetikkuma.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_yetikkuma.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_yetikkuma_background.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_yetikkuma_background.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_yetikkuma_foreground.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_yetikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_yetikkuma_monochrome.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_yetikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_yetikkuma_round.webp androidApp/src/main/res/mipmap-mdpi/ic_launcher_yetikkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_aoharukkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_aoharukkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_aoharukkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_aoharukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_aoharukkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_aoharukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_aoharukkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_aoharukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_aoharukkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_aoharukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_doragokkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_doragokkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_doragokkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_doragokkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_doragokkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_doragokkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_doragokkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_doragokkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_doragokkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_doragokkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_enjekkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_enjekkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_enjekkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_enjekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_enjekkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_enjekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_enjekkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_enjekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_enjekkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_enjekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_higekkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_higekkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_higekkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_higekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_higekkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_higekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_higekkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_higekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_higekkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_higekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_kurobarakkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_kurobarakkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_kurobarakkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_kurobarakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_kurobarakkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_kurobarakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_kurobarakkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_kurobarakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_kurobarakkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_kurobarakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_lemonkkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_lemonkkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_lemonkkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_lemonkkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_lemonkkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_lemonkkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_lemonkkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_lemonkkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_lemonkkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_lemonkkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_limekkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_limekkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_limekkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_limekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_limekkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_limekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_limekkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_limekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_limekkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_limekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_meganekkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_meganekkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_meganekkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_meganekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_meganekkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_meganekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_meganekkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_meganekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_meganekkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_meganekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_mitsuyoshikkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_ninjakkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_ninjakkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_ninjakkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_ninjakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_ninjakkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_ninjakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_ninjakkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_ninjakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_ninjakkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_ninjakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_onemukkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_onemukkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_onemukkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_onemukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_onemukkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_onemukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_onemukkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_onemukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_onemukkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_onemukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_pandakkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_pandakkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_pandakkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_pandakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_pandakkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_pandakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_pandakkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_pandakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_pandakkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_pandakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_risukkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_risukkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_risukkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_risukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_risukkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_risukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_risukkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_risukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_risukkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_risukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_tanukkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_tanukkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_tanukkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_tanukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_tanukkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_tanukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_tanukkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_tanukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_tanukkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_tanukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_yetikkuma.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_yetikkuma.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_yetikkuma_background.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_yetikkuma_background.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_yetikkuma_foreground.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_yetikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_yetikkuma_monochrome.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_yetikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_yetikkuma_round.webp androidApp/src/main/res/mipmap-xhdpi/ic_launcher_yetikkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_aoharukkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_aoharukkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_aoharukkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_aoharukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_aoharukkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_aoharukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_aoharukkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_aoharukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_aoharukkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_aoharukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_doragokkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_doragokkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_doragokkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_doragokkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_doragokkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_doragokkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_doragokkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_doragokkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_doragokkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_doragokkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_enjekkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_enjekkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_enjekkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_enjekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_enjekkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_enjekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_enjekkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_enjekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_enjekkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_enjekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_higekkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_higekkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_higekkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_higekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_higekkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_higekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_higekkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_higekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_higekkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_higekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_kurobarakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_lemonkkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_lemonkkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_lemonkkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_lemonkkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_lemonkkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_lemonkkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_lemonkkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_lemonkkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_lemonkkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_lemonkkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_limekkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_limekkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_limekkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_limekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_limekkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_limekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_limekkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_limekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_limekkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_limekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_meganekkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_meganekkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_meganekkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_meganekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_meganekkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_meganekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_meganekkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_meganekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_meganekkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_meganekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_mitsuyoshikkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_ninjakkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_ninjakkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_ninjakkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_ninjakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_ninjakkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_ninjakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_ninjakkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_ninjakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_ninjakkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_ninjakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_onemukkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_onemukkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_onemukkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_onemukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_onemukkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_onemukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_onemukkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_onemukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_onemukkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_onemukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_pandakkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_pandakkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_pandakkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_pandakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_pandakkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_pandakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_pandakkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_pandakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_pandakkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_pandakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_risukkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_risukkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_risukkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_risukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_risukkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_risukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_risukkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_risukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_risukkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_risukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_tanukkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_tanukkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_tanukkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_tanukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_tanukkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_tanukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_tanukkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_tanukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_tanukkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_tanukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_yetikkuma.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_yetikkuma.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_yetikkuma_background.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_yetikkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_yetikkuma_foreground.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_yetikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_yetikkuma_monochrome.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_yetikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_yetikkuma_round.webp androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_yetikkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_aoharukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_doragokkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_doragokkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_doragokkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_doragokkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_doragokkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_doragokkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_doragokkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_doragokkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_doragokkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_doragokkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_enjekkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_enjekkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_enjekkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_enjekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_enjekkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_enjekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_enjekkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_enjekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_enjekkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_enjekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_higekkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_higekkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_higekkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_higekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_higekkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_higekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_higekkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_higekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_higekkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_higekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_kurobarakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_lemonkkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_limekkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_limekkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_limekkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_limekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_limekkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_limekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_limekkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_limekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_limekkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_limekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_meganekkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_meganekkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_meganekkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_meganekkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_meganekkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_meganekkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_meganekkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_meganekkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_meganekkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_meganekkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_mitsuyoshikkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_ninjakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_onemukkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_onemukkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_onemukkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_onemukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_onemukkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_onemukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_onemukkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_onemukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_onemukkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_onemukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_pandakkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_pandakkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_pandakkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_pandakkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_pandakkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_pandakkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_pandakkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_pandakkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_pandakkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_pandakkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_risukkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_risukkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_risukkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_risukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_risukkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_risukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_risukkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_risukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_risukkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_risukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_tanukkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_tanukkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_tanukkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_tanukkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_tanukkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_tanukkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_tanukkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_tanukkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_tanukkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_tanukkuma_round.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_yetikkuma.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_yetikkuma.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_yetikkuma_background.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_yetikkuma_background.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_yetikkuma_foreground.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_yetikkuma_foreground.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_yetikkuma_monochrome.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_yetikkuma_monochrome.webp
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_yetikkuma_round.webp androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_yetikkuma_round.webp
composeApp/src/androidMain/res/mono.png androidApp/src/main/res/mono.png
composeApp/src/androidMain/res/values/strings.xml androidApp/src/main/res/values/strings.xml
+30 -21
composeApp/src/commonMain/kotlin/com/derakkuma/App.kt
··· 1 1 package com.derakkuma 2 2 3 + import com.derakkuma.ui.components.ExpressiveLoadingIndicator 4 + import com.derakkuma.ui.components.ExpressiveBadgedBox 5 + import com.derakkuma.ui.components.ExpressiveBadge 6 + import com.derakkuma.ui.components.ExpressiveScaffold 7 + import com.derakkuma.ui.components.expressiveNavigationBarItemColors 8 + import com.derakkuma.ui.components.ExpressiveNavigationBarItem 9 + import com.derakkuma.ui.components.ExpressiveNavigationBar 10 + import com.derakkuma.ui.components.ExpressiveTopLoadingIndicator 11 + import com.derakkuma.ui.components.expressiveScreenBackground 3 12 import androidx.compose.animation.core.LinearEasing 4 13 import androidx.compose.animation.core.animateFloat 5 14 import androidx.compose.animation.core.infiniteRepeatable ··· 61 70 import com.derakkuma.ui.images.MaimaiAssets 62 71 import com.derakkuma.ui.images.UiIconPreloadImages 63 72 import com.derakkuma.ui.tabs.* 64 - import com.derakkuma.ui.theme.DarkText 65 73 import com.derakkuma.ui.theme.DerakkumaTheme 66 - import com.derakkuma.ui.theme.MaimaiBlue 67 - import com.derakkuma.ui.theme.MutedText 74 + import com.derakkuma.ui.theme.ExpressiveBubblegum 75 + import com.derakkuma.ui.theme.ExpressiveLemon 68 76 import com.swmansion.kmpmaps.core.MapConfiguration 69 77 import kotlinx.coroutines.launch 70 78 import kotlinx.serialization.Serializable ··· 220 228 DerakkumaTheme { 221 229 if (checkingAuth || !uiIconsPreloaded) { 222 230 Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 223 - CircularProgressIndicator(color = MaterialTheme.colorScheme.onBackground) 231 + ExpressiveLoadingIndicator(color = MaterialTheme.colorScheme.onBackground) 224 232 } 225 233 return@DerakkumaTheme 226 234 } ··· 257 265 ) 258 266 } else if (refreshingSession) { 259 267 Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 260 - CircularProgressIndicator(color = MaterialTheme.colorScheme.onBackground) 268 + ExpressiveLoadingIndicator(color = MaterialTheme.colorScheme.onBackground) 261 269 } 262 270 } else { 263 271 val cookie = auth!!.cookie ··· 328 336 } 329 337 } 330 338 331 - Scaffold( 339 + ExpressiveScaffold( 332 340 bottomBar = { 333 341 Box { 334 - NavigationBar( 342 + val navigationContentColor = MaterialTheme.colorScheme.onPrimary 343 + ExpressiveNavigationBar( 335 344 containerColor = Color(0xFF7CBC29), 336 - contentColor = Color.White, 345 + contentColor = navigationContentColor, 337 346 ) { 338 347 Tab.entries.forEach { tab -> 339 - NavigationBarItem( 348 + val selectedContainer = navigationContentColor.copy(alpha = if (selectedTab == tab) 0.38f else 0.18f) 349 + ExpressiveNavigationBarItem( 340 350 selected = selectedTab == tab, 341 351 onClick = { 342 352 navController.navigate(tab.route) { ··· 349 359 }, 350 360 label = { Text(tab.label) }, 351 361 icon = { 352 - BadgedBox( 362 + ExpressiveBadgedBox( 353 363 badge = { 354 364 if (tab == Tab.Friends && pendingFriendRequestCount > 0) { 355 - Badge(containerColor = Color(0xFFFB4B78), contentColor = Color.White) { 365 + ExpressiveBadge { 356 366 Text(pendingFriendRequestCount.toString()) 357 367 } 358 368 } ··· 372 382 imageVector = tab.flatIcon, 373 383 contentDescription = tab.label, 374 384 modifier = Modifier.size(26.dp).alpha(if (selectedTab == tab) 1f else 0.6f), 375 - tint = Color.White, 385 + tint = navigationContentColor, 376 386 ) 377 387 } 378 388 } 379 389 }, 380 - colors = NavigationBarItemDefaults.colors( 381 - indicatorColor = Color.White.copy(alpha = 0.2f), 382 - selectedIconColor = Color.White, 383 - selectedTextColor = Color.White, 384 - unselectedIconColor = Color.White.copy(alpha = 0.6f), 385 - unselectedTextColor = Color.White.copy(alpha = 0.6f), 390 + colors = expressiveNavigationBarItemColors( 391 + contentColor = navigationContentColor, 392 + selectedContainerColor = selectedContainer, 386 393 ), 387 394 ) 388 395 } ··· 391 398 } 392 399 }, 393 400 ) { padding -> 394 - Surface( 395 - modifier = Modifier.fillMaxSize().padding(padding), 396 - color = MaterialTheme.colorScheme.background, 401 + Box( 402 + modifier = Modifier.fillMaxSize().padding(padding).expressiveScreenBackground(), 397 403 ) { 398 404 Column(Modifier.fillMaxSize()) { 405 + if (refreshingSession) { 406 + ExpressiveTopLoadingIndicator(color = MaterialTheme.colorScheme.primary) 407 + } 399 408 if (maintenanceMode) { 400 409 MaintenanceBanner() 401 410 }
+76 -67
composeApp/src/commonMain/kotlin/com/derakkuma/atproto/AtProtoSettings.kt
··· 1 1 package com.derakkuma.atproto 2 2 3 + import com.derakkuma.ui.components.ExpressiveButton 4 + import com.derakkuma.ui.components.ExpressiveGlassCard 5 + import com.derakkuma.ui.components.ExpressiveHorizontalDivider 6 + import com.derakkuma.ui.components.ExpressiveLinearProgressIndicator 7 + import com.derakkuma.ui.components.ExpressiveOutlinedButton 8 + import com.derakkuma.ui.components.ExpressiveSwitch 9 + import com.derakkuma.ui.components.ExpressiveTextButton 10 + import com.derakkuma.ui.components.ExpressiveAlertDialog 11 + import com.derakkuma.ui.components.ExpressiveDropdownMenu 12 + import com.derakkuma.ui.components.ExpressiveDropdownMenuItem 13 + import com.derakkuma.ui.components.ExpressiveTextField 3 14 import androidx.compose.animation.core.animateFloatAsState 4 15 import androidx.compose.animation.core.tween 5 16 import androidx.compose.foundation.layout.* 6 - import androidx.compose.foundation.shape.CircleShape 7 17 import androidx.compose.foundation.shape.RoundedCornerShape 8 18 import androidx.compose.material.icons.Icons 9 19 import androidx.compose.material.icons.filled.CloudSync ··· 13 23 import androidx.compose.runtime.* 14 24 import androidx.compose.ui.Alignment 15 25 import androidx.compose.ui.Modifier 26 + import androidx.compose.ui.graphics.Color 16 27 import androidx.compose.ui.draw.clip 17 28 import androidx.compose.ui.layout.ContentScale 18 29 import androidx.compose.ui.text.font.FontWeight ··· 215 226 verticalAlignment = Alignment.CenterVertically, 216 227 ) { 217 228 Column(modifier = Modifier.weight(1f)) { 218 - Text("Linked Account", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 219 - Text("@${account!!.handle}", fontSize = 12.sp, color = MutedText) 229 + Text("Linked Account", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 230 + Text("@${account!!.handle}", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 220 231 } 221 - OutlinedButton( 232 + ExpressiveOutlinedButton( 222 233 onClick = { 223 234 scope.launch { 224 235 try { ··· 232 243 } 233 244 } 234 245 }, 235 - shape = RoundedCornerShape(8.dp), 236 - colors = ButtonDefaults.outlinedButtonColors(contentColor = DangerRed), 246 + colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error), 237 247 ) { 238 248 Icon(Icons.Default.LinkOff, contentDescription = null, modifier = Modifier.size(16.dp)) 239 249 Spacer(Modifier.width(4.dp)) ··· 241 251 } 242 252 } 243 253 244 - if (showConfig) HorizontalDivider(color = MutedText.copy(alpha = 0.12f)) 254 + if (showConfig) ExpressiveHorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) 245 255 246 256 if (hasSynced) { 247 - OutlinedButton( 257 + ExpressiveOutlinedButton( 248 258 onClick = { configExpanded = !configExpanded }, 249 259 modifier = Modifier.fillMaxWidth(), 250 - shape = RoundedCornerShape(8.dp), 251 260 ) { 252 261 Text(if (configExpanded) "Hide configuration" else "Configure", fontSize = 13.sp) 253 262 } ··· 255 264 256 265 // Publish toggles 257 266 if (showConfig) { 258 - Text("Publish to AT Protocol", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 259 - Text("Control what data gets published to your PDS", fontSize = 11.sp, color = MutedText) 267 + Text("Publish to AT Protocol", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 268 + Text("Control what data gets published to your PDS", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 260 269 261 270 PublishToggle("Profile", "Player name, rating, images, comment", settings.publishProfile) { checked -> 262 271 settings = settings.copy(publishProfile = checked, publishFriendCode = if (checked) settings.publishFriendCode else false) ··· 302 311 configureAtProtoBackgroundSync(account != null && checked) 303 312 } 304 313 if (settings.backgroundSyncEnabled && !canScheduleAtProtoBackgroundSync()) { 305 - Text("Android needs exact alarm permission for reliable 12-hour background sync.", fontSize = 11.sp, color = MutedText) 306 - TextButton(onClick = { openAtProtoBackgroundSyncSettings() }) { 314 + Text("Android needs exact alarm permission for reliable 12-hour background sync.", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 315 + ExpressiveTextButton(onClick = { openAtProtoBackgroundSyncSettings() }) { 307 316 Text("Open alarm permission settings", fontSize = 12.sp) 308 317 } 309 318 } 310 319 } 311 320 312 - if (showConfig) HorizontalDivider(color = MutedText.copy(alpha = 0.12f)) 321 + if (showConfig) ExpressiveHorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) 313 322 314 323 // Manual sync button 315 - Button( 324 + ExpressiveButton( 316 325 onClick = { 317 326 scope.launch { 318 327 syncing = true ··· 346 355 } 347 356 }, 348 357 modifier = Modifier.fillMaxWidth(), 349 - shape = RoundedCornerShape(8.dp), 350 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), 358 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 351 359 enabled = !syncing, 352 360 ) { 353 361 Icon(Icons.Default.CloudSync, contentDescription = null, modifier = Modifier.size(18.dp)) ··· 362 370 animationSpec = tween(durationMillis = 450), 363 371 label = "atproto-sync-progress", 364 372 ) 365 - LinearProgressIndicator( 373 + ExpressiveLinearProgressIndicator( 366 374 progress = { animatedProgress }, 367 375 modifier = Modifier.fillMaxWidth(), 368 - color = PrimaryBlue, 376 + color = MaterialTheme.colorScheme.primary, 369 377 ) 370 - Text(syncProgress!!.status, fontSize = 11.sp, color = MutedText) 371 - Text("Don't close the app while syncing!", fontSize = 11.sp, color = DangerRed) 378 + Text(syncProgress!!.status, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 379 + Text("Don't close the app while syncing!", fontSize = 11.sp, color = MaterialTheme.colorScheme.error) 372 380 } 373 381 374 382 // Sync status 375 383 if (showConfig && syncState.lastSyncTime.isNotEmpty()) { 376 - Text("Last synced: ${syncState.lastSyncTime.take(19)}", fontSize = 11.sp, color = MutedText) 384 + Text("Last synced: ${syncState.lastSyncTime.take(19)}", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 377 385 val plays = syncState.publishedPlayCount 378 386 val bests = syncState.publishedBestCount 379 387 val friends = syncState.publishedFriendCount 380 388 val favorites = syncState.publishedFavoriteSongCount 381 389 val circles = syncState.publishedCircleRecordCount 382 390 if (plays > 0 || bests > 0 || friends > 0 || favorites > 0 || circles > 0) { 383 - Text("Total published: $plays plays, $bests bests, $friends friends, $favorites favorites, $circles circle records", fontSize = 11.sp, color = MutedText) 391 + Text("Total published: $plays plays, $bests bests, $friends friends, $favorites favorites, $circles circle records", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 384 392 } 385 393 val queued = cacheStore.getPendingAtProtoWrites().size 386 394 if (queued > 0) { 387 - Text("$queued write${if (queued == 1) "" else "s"} queued for retry", fontSize = 11.sp, color = MutedText) 395 + Text("$queued write${if (queued == 1) "" else "s"} queued for retry", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 388 396 } 389 397 if (syncState.lastError.isNotBlank()) { 390 - Text("Last ATProto error: ${syncState.lastError}", fontSize = 11.sp, color = DangerRed) 398 + Text("Last ATProto error: ${syncState.lastError}", fontSize = 11.sp, color = MaterialTheme.colorScheme.error) 391 399 } 392 400 } 393 401 } ··· 431 439 } else { 432 440 // Not linked 433 441 SectionCard { 434 - Text("Link your Bluesky/ATProtocol account to publish your plays on the Atmosphere and connect with friends!", fontSize = 12.sp, color = MutedText) 442 + Text("Link your Bluesky/ATProtocol account to publish your plays on the Atmosphere and connect with friends!", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 435 443 Box(modifier = Modifier.fillMaxWidth()) { 436 - OutlinedTextField( 444 + ExpressiveTextField( 437 445 value = handleOrDid, 438 446 onValueChange = { handleOrDid = it }, 439 447 modifier = Modifier.fillMaxWidth(), 440 448 singleLine = true, 441 449 label = { Text("Handle or DID") }, 442 450 placeholder = { Text("you.bsky.social") }, 443 - ) 444 - DropdownMenu( 451 + ) 452 + ExpressiveDropdownMenu( 445 453 expanded = handleSuggestions.isNotEmpty(), 446 454 onDismissRequest = { handleSuggestions = emptyList() }, 447 455 modifier = Modifier.fillMaxWidth(0.92f), 448 456 ) { 449 - handleSuggestions.forEach { suggestion -> 450 - DropdownMenuItem( 457 + handleSuggestions.forEachIndexed { index, suggestion -> 458 + ExpressiveDropdownMenuItem( 459 + selected = false, 460 + index = index, 461 + lastIndex = handleSuggestions.lastIndex, 451 462 text = { 452 463 Row( 453 464 verticalAlignment = Alignment.CenterVertically, ··· 456 467 if (suggestion.avatar.isNotBlank()) { 457 468 MaimaiRemoteImage( 458 469 url = suggestion.avatar, 459 - modifier = Modifier.size(34.dp).clip(CircleShape), 470 + modifier = Modifier.size(34.dp).clip(RoundedCornerShape(2.dp)), 460 471 contentDescription = suggestion.displayName.ifBlank { suggestion.handle }, 461 472 contentScale = ContentScale.Crop, 462 473 ) 463 474 } 464 475 Column { 465 476 Text("@${suggestion.handle}", fontSize = 13.sp, fontWeight = FontWeight.SemiBold) 466 - if (suggestion.displayName.isNotBlank()) Text(suggestion.displayName, fontSize = 11.sp, color = MutedText) 477 + if (suggestion.displayName.isNotBlank()) Text(suggestion.displayName, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 467 478 } 468 479 } 469 480 }, ··· 475 486 } 476 487 } 477 488 } 478 - Button( 489 + ExpressiveButton( 479 490 onClick = { 480 491 scope.launch { 481 492 linking = true ··· 499 510 } 500 511 }, 501 512 modifier = Modifier.fillMaxWidth(), 502 - shape = RoundedCornerShape(8.dp), 503 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), 513 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 504 514 enabled = !linking, 505 515 ) { 506 516 Icon(Icons.Default.Link, contentDescription = null, modifier = Modifier.size(18.dp)) ··· 512 522 513 523 snackMsg?.let { 514 524 SectionCard(contentPadding = 12.dp) { 515 - Text(it, fontSize = 12.sp, color = if (it.startsWith("Error:")) DangerRed else MutedText) 525 + Text(it, fontSize = 12.sp, color = if (it.startsWith("Error:")) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant) 516 526 } 517 527 } 518 528 } else { 519 529 // Platform not supported (iOS stub) 520 530 SectionCard { 521 - Text("AT Protocol integration is unavailable in this build.", fontSize = 12.sp, color = MutedText) 531 + Text("AT Protocol integration is unavailable in this build.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 522 532 } 523 533 } 524 534 } ··· 532 542 } 533 543 534 544 if (showFriendCodePublishConfirm) { 535 - AlertDialog( 545 + ExpressiveAlertDialog( 536 546 onDismissRequest = { showFriendCodePublishConfirm = false }, 537 547 title = { Text("Publish friend code?") }, 538 548 text = { Text("This will add your maimai friend code to your public AT Protocol profile, which lets Bluesky friends discover you and send maimai friend requests through Derakkuma. Even if you remove it later, other apps or indexers may have already copied it.") }, 539 549 confirmButton = { 540 - TextButton( 550 + ExpressiveTextButton( 541 551 onClick = { 542 552 settings = settings.copy(publishProfile = true, publishFriendCode = true) 543 553 cacheStore.putAtProtoPublishSettings(settings) ··· 546 556 ) { Text("Publish") } 547 557 }, 548 558 dismissButton = { 549 - TextButton(onClick = { showFriendCodePublishConfirm = false }) { Text("Cancel") } 559 + ExpressiveTextButton(onClick = { showFriendCodePublishConfirm = false }) { Text("Cancel") } 550 560 }, 551 561 ) 552 562 } ··· 582 592 contentPadding: androidx.compose.ui.unit.Dp = 16.dp, 583 593 content: @Composable ColumnScope.() -> Unit, 584 594 ) { 585 - Card( 595 + ExpressiveGlassCard( 586 596 modifier = Modifier.fillMaxWidth(), 587 - shape = RoundedCornerShape(8.dp), 588 - colors = CardDefaults.cardColors(containerColor = CardWhite), 597 + cornerRadius = 24.dp, 598 + containerColor = Color.White, 589 599 ) { 590 600 Column( 591 601 modifier = Modifier.padding(contentPadding), ··· 602 612 error: String?, 603 613 ) { 604 614 Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { 605 - Text("Atmosphere Activity", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 615 + Text("Atmosphere Activity", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 606 616 if (error != null) { 607 617 Text(error, fontSize = 12.sp, color = MaterialTheme.colorScheme.error) 608 618 return@Column ··· 614 624 SmallStat("Avg", if (stats.bestCount > 0) "${formatScore(stats.averageAchievement)}%" else "--") 615 625 } 616 626 } 617 - Text("Record Feed", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 618 - Text("Recent records from people you follow plus everyone on the Atmosphere.", fontSize = 11.sp, color = MutedText) 627 + Text("Record Feed", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 628 + Text("Recent records from people you follow plus everyone on the Atmosphere.", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 619 629 if (feed.isEmpty()) { 620 - Text("No record activity yet... get 'em on Derakkuma! :D", fontSize = 12.sp, color = MutedText) 630 + Text("No record activity yet... get 'em on Derakkuma! :D", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 621 631 } else { 622 632 feed.take(20).forEach { play -> 623 633 Column(Modifier.fillMaxWidth().padding(vertical = 2.dp)) { 624 - Text(play.songName.ifBlank { "Unknown song" }, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = DarkText, maxLines = 1) 634 + Text(play.songName.ifBlank { "Unknown song" }, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface, maxLines = 1) 625 635 val player = play.playerName.ifBlank { play.did.take(16) + "…" } 626 636 val source = play.source.takeIf { it.isNotBlank() }?.let { " • $it" }.orEmpty() 627 - Text("$player • ${play.difficulty} ${play.level} • ${formatScore(play.achievement.toDoubleOrNull() ?: 0.0)}%$source", fontSize = 11.sp, color = MutedText, maxLines = 1) 637 + Text("$player • ${play.difficulty} ${play.level} • ${formatScore(play.achievement.toDoubleOrNull() ?: 0.0)}%$source", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) 628 638 } 629 639 } 630 640 } ··· 634 644 @Composable 635 645 private fun SmallStat(label: String, value: String) { 636 646 Column(horizontalAlignment = Alignment.CenterHorizontally) { 637 - Text(value, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = PrimaryBlue) 638 - Text(label, fontSize = 10.sp, color = MutedText) 647 + Text(value, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) 648 + Text(label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 639 649 } 640 650 } 641 651 ··· 649 659 onRequest: (AtProtoSocialProfile) -> Unit, 650 660 ) { 651 661 Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { 652 - Text("Social Bridge", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 653 - Text("Discovery uses published Derakkuma profiles.", fontSize = 11.sp, color = MutedText) 662 + Text("Social Bridge", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 663 + Text("Discovery uses published Derakkuma profiles.", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 654 664 655 - if (loading) LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = PrimaryBlue) 665 + if (loading) ExpressiveLinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.primary) 656 666 if (error != null) Text(error, fontSize = 12.sp, color = MaterialTheme.colorScheme.error) 657 667 658 - Text("Maimai friends on Bluesky", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 668 + Text("Maimai friends on Bluesky", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 659 669 if (maimaiFriendMatches.isEmpty()) { 660 - Text(if (loading) "Looking for matches..." else "No maimai friends matched a published Bluesky profile yet.", fontSize = 12.sp, color = MutedText) 670 + Text(if (loading) "Looking for matches..." else "No maimai friends matched a published Bluesky profile yet.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 661 671 } else { 662 672 maimaiFriendMatches.forEach { match -> 663 673 SocialActionRow( ··· 670 680 } 671 681 } 672 682 673 - Text("Bluesky follows on maimai", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 683 + Text("Bluesky follows on maimai", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 674 684 if (bskyFollowMatches.isEmpty()) { 675 - Text(if (loading) "Looking for matches..." else "No Bluesky follows matched a published Derakkuma profile yet.", fontSize = 12.sp, color = MutedText) 685 + Text(if (loading) "Looking for matches..." else "No Bluesky follows matched a published Derakkuma profile yet.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 676 686 } else { 677 687 bskyFollowMatches.forEach { match -> 678 688 val label = match.profile.playerName.ifBlank { match.follow.displayName.ifBlank { match.follow.handle } } ··· 698 708 ) { 699 709 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { 700 710 Column(Modifier.weight(1f)) { 701 - Text(title, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = DarkText, maxLines = 1) 702 - Text(subtitle, fontSize = 11.sp, color = MutedText, maxLines = 1) 711 + Text(title, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface, maxLines = 1) 712 + Text(subtitle, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) 703 713 } 704 - OutlinedButton(onClick = onClick, enabled = enabled, shape = RoundedCornerShape(8.dp)) { 714 + ExpressiveOutlinedButton(onClick = onClick, enabled = enabled) { 705 715 Text(action, fontSize = 12.sp) 706 716 } 707 717 } ··· 721 731 verticalAlignment = Alignment.CenterVertically, 722 732 ) { 723 733 Column(modifier = Modifier.weight(1f)) { 724 - Text(label, fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 725 - Text(description, fontSize = 11.sp, color = MutedText) 734 + Text(label, fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 735 + Text(description, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 726 736 } 727 - Switch( 737 + ExpressiveSwitch( 728 738 checked = checked, 729 739 onCheckedChange = onCheckedChange, 730 - enabled = enabled, 731 - colors = SwitchDefaults.colors(checkedTrackColor = PrimaryBlue), 740 + enabled = enabled 732 741 ) 733 742 } 734 743 }
+25 -28
composeApp/src/commonMain/kotlin/com/derakkuma/ui/OnboardingScreen.kt
··· 10 10 import androidx.compose.foundation.layout.* 11 11 import androidx.compose.foundation.rememberScrollState 12 12 import androidx.compose.foundation.shape.CircleShape 13 - import androidx.compose.foundation.shape.RoundedCornerShape 14 13 import androidx.compose.foundation.verticalScroll 15 14 import androidx.compose.material.icons.Icons 16 15 import androidx.compose.material3.* ··· 26 25 import androidx.compose.ui.unit.sp 27 26 import com.derakkuma.atproto.openOAuthUrl 28 27 import com.derakkuma.ui.components.MaintenanceBanner 28 + import com.derakkuma.ui.components.ExpressiveButton 29 + import com.derakkuma.ui.components.ExpressiveGlassCard 30 + import com.derakkuma.ui.components.ExpressiveIconSurface 31 + import com.derakkuma.ui.components.ExpressiveCheckbox 32 + import com.derakkuma.ui.components.ExpressiveTextButton 33 + import com.derakkuma.ui.components.expressiveScreenBackground 29 34 import com.derakkuma.ui.images.BundledIcon 30 35 import com.derakkuma.ui.images.BundledImage 31 36 import com.derakkuma.ui.images.DerakkumaGroupAssets 32 - import com.derakkuma.ui.theme.CardWhite 33 - import com.derakkuma.ui.theme.DarkText 34 - import com.derakkuma.ui.theme.MaimaiBlue 35 - import com.derakkuma.ui.theme.MutedText 36 - import com.derakkuma.ui.theme.PrimaryBlue 37 37 38 38 @Composable 39 39 fun OnboardingScreen(onComplete: () -> Unit, maintenanceMode: Boolean = false) { 40 40 var accepted by remember { mutableStateOf(false) } 41 41 42 - Surface(color = MaimaiBlue, modifier = Modifier.fillMaxSize()) { 42 + Box(modifier = Modifier.fillMaxSize().expressiveScreenBackground()) { 43 43 Column( 44 44 modifier = Modifier 45 45 .fillMaxSize() ··· 53 53 MaintenanceBanner() 54 54 } 55 55 56 - Card( 57 - shape = RoundedCornerShape(28.dp), 58 - colors = CardDefaults.cardColors(containerColor = CardWhite), 56 + ExpressiveGlassCard( 59 57 modifier = Modifier.fillMaxWidth(), 60 58 ) { 61 59 Column( ··· 66 64 DerakkumaGroupMascots() 67 65 68 66 Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(6.dp)) { 69 - Text("Welcome to Derakkuma", color = DarkText, fontWeight = FontWeight.Bold, fontSize = 22.sp, textAlign = TextAlign.Center) 67 + Text("Welcome to Derakkuma", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 22.sp, textAlign = TextAlign.Center) 70 68 Text( 71 69 "Your maimai DX NET companion!", 72 - color = MutedText, 70 + color = MaterialTheme.colorScheme.onSurfaceVariant, 73 71 fontSize = 14.sp, 74 72 lineHeight = 19.sp, 75 73 textAlign = TextAlign.Center, ··· 92 90 text = "Your maimai DX NET session and cached data are stored only on this device. They never leave your device unless you explicitly enable sharing/syncing features.", 93 91 ) 94 92 95 - Card( 96 - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.08f)), 97 - shape = RoundedCornerShape(18.dp), 93 + ExpressiveGlassCard( 98 94 modifier = Modifier.fillMaxWidth(), 95 + cornerRadius = 28.dp, 96 + containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.42f), 99 97 ) { 100 98 Column(Modifier.fillMaxWidth().padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 2.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 101 - Text("Before we start", color = DarkText, fontWeight = FontWeight.Bold, fontSize = 12.sp) 99 + Text("Before we start", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 12.sp) 102 100 Text( 103 101 "Derakkuma is an unofficial maimai DX NET client and is not affiliated with or endorsed by SEGA. By continuing, you agree that your use of maimai DX NET through this app follows maimai DX NET Terms of Service and Privacy Policy.", 104 - color = MutedText, 102 + color = MaterialTheme.colorScheme.onSurfaceVariant, 105 103 fontSize = 10.sp, 106 104 lineHeight = 17.sp, 107 105 ) 108 106 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 109 - TextButton(onClick = { openOAuthUrl("https://derakkuma.com/terms") }) { 107 + ExpressiveTextButton(onClick = { openOAuthUrl("https://derakkuma.com/terms") }) { 110 108 Text("Terms of Service", fontSize = 11.sp) 111 109 } 112 - TextButton(onClick = { openOAuthUrl("https://derakkuma.com/privacy") }) { 110 + ExpressiveTextButton(onClick = { openOAuthUrl("https://derakkuma.com/privacy") }) { 113 111 Text("Privacy Policy", fontSize = 11.sp) 114 112 } 115 113 } 116 114 Row(verticalAlignment = Alignment.CenterVertically) { 117 - Checkbox(checked = accepted, onCheckedChange = { accepted = it }) 118 - Text("I understand and agree", color = DarkText, fontSize = 13.sp) 115 + ExpressiveCheckbox(checked = accepted, onCheckedChange = { accepted = it }) 116 + Text("I understand and agree", color = MaterialTheme.colorScheme.onSurface, fontSize = 13.sp) 119 117 } 120 118 } 121 119 } 122 120 123 - Button( 121 + ExpressiveButton( 124 122 onClick = onComplete, 125 123 enabled = accepted && !maintenanceMode, 126 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), 124 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 127 125 modifier = Modifier.fillMaxWidth().height(52.dp), 128 - shape = RoundedCornerShape(16.dp), 129 126 ) { 130 127 Text("Get started", fontWeight = FontWeight.Bold) 131 128 } ··· 203 200 @Composable 204 201 private fun OnboardingPoint(icon: Any, title: String, text: String) { 205 202 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top) { 206 - Surface(shape = CircleShape, color = PrimaryBlue.copy(alpha = 0.12f), modifier = Modifier.size(40.dp)) { 203 + ExpressiveIconSurface(size = 40.dp, containerColor = MaterialTheme.colorScheme.primaryContainer) { 207 204 Box(contentAlignment = Alignment.Center) { 208 205 when (icon) { 209 - is androidx.compose.ui.graphics.vector.ImageVector -> Icon(icon, contentDescription = null, tint = PrimaryBlue, modifier = Modifier.size(21.dp)) 206 + is androidx.compose.ui.graphics.vector.ImageVector -> Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(21.dp)) 210 207 is BundledImage -> BundledIcon(icon, contentDescription = null, modifier = Modifier.size(24.dp)) 211 208 } 212 209 } 213 210 } 214 211 Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { 215 - Text(title, color = DarkText, fontWeight = FontWeight.Bold, fontSize = 14.sp) 216 - Text(text, color = MutedText, fontSize = 12.sp, lineHeight = 17.sp) 212 + Text(title, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 14.sp) 213 + Text(text, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, lineHeight = 17.sp) 217 214 } 218 215 } 219 216 }
+758
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/ExpressiveControls.kt
··· 1 + package com.derakkuma.ui.components 2 + 3 + import androidx.compose.animation.core.AnimationSpec 4 + import androidx.compose.animation.core.Spring 5 + import androidx.compose.animation.core.spring 6 + import androidx.compose.animation.core.tween 7 + import androidx.compose.foundation.interaction.MutableInteractionSource 8 + import androidx.compose.foundation.interaction.collectIsPressedAsState 9 + import androidx.compose.foundation.layout.ColumnScope 10 + import androidx.compose.foundation.layout.BoxScope 11 + import androidx.compose.foundation.layout.fillMaxWidth 12 + import androidx.compose.foundation.layout.height 13 + import androidx.compose.foundation.layout.PaddingValues 14 + import androidx.compose.foundation.layout.RowScope 15 + import androidx.compose.foundation.layout.WindowInsets 16 + import androidx.compose.foundation.layout.safeDrawing 17 + import androidx.compose.foundation.layout.size 18 + import androidx.compose.material3.Button 19 + import androidx.compose.material3.ButtonColors 20 + import androidx.compose.material3.ButtonDefaults 21 + import androidx.compose.material3.AssistChip 22 + import androidx.compose.material3.AssistChipDefaults 23 + import androidx.compose.material3.AlertDialog 24 + import androidx.compose.material3.Badge 25 + import androidx.compose.material3.BadgedBox 26 + import androidx.compose.material3.Checkbox 27 + import androidx.compose.material3.CheckboxDefaults 28 + import androidx.compose.material3.CircularProgressIndicator 29 + import androidx.compose.material3.ExperimentalMaterial3Api 30 + import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 31 + import androidx.compose.material3.DropdownMenu 32 + import androidx.compose.material3.DropdownMenuItem 33 + import androidx.compose.material3.FilterChip 34 + import androidx.compose.material3.FilterChipDefaults 35 + import androidx.compose.material3.FabPosition 36 + import androidx.compose.material3.FilledTonalIconButton 37 + import androidx.compose.material3.FloatingActionButton 38 + import androidx.compose.material3.HorizontalDivider 39 + import androidx.compose.material3.LinearWavyProgressIndicator 40 + import androidx.compose.material3.MaterialTheme 41 + import androidx.compose.material3.MenuDefaults 42 + import androidx.compose.material3.NavigationBar 43 + import androidx.compose.material3.NavigationBarItem 44 + import androidx.compose.material3.NavigationBarItemColors 45 + import androidx.compose.material3.NavigationBarItemDefaults 46 + import androidx.compose.material3.OutlinedButton 47 + import androidx.compose.material3.Slider 48 + import androidx.compose.material3.SliderDefaults 49 + import androidx.compose.material3.Snackbar 50 + import androidx.compose.material3.Switch 51 + import androidx.compose.material3.SwitchDefaults 52 + import androidx.compose.material3.TextButton 53 + import androidx.compose.material3.TextField 54 + import androidx.compose.material3.TextFieldColors 55 + import androidx.compose.material3.TextFieldDefaults 56 + import androidx.compose.material3.ToggleButton 57 + import androidx.compose.material3.ToggleButtonShapes 58 + import androidx.compose.material3.WavyProgressIndicatorDefaults 59 + import androidx.compose.material3.pulltorefresh.PullToRefreshBox 60 + import androidx.compose.runtime.Composable 61 + import androidx.compose.runtime.getValue 62 + import androidx.compose.runtime.remember 63 + import androidx.compose.ui.Modifier 64 + import androidx.compose.ui.draw.scale 65 + import androidx.compose.ui.graphics.Color 66 + import androidx.compose.ui.graphics.Shape 67 + import androidx.compose.ui.text.input.VisualTransformation 68 + import androidx.compose.ui.unit.Dp 69 + import androidx.compose.ui.unit.DpSize 70 + import androidx.compose.ui.unit.dp 71 + 72 + import androidx.compose.material3.SearchBar 73 + import androidx.compose.material3.SearchBarColors 74 + import androidx.compose.material3.SearchBarDefaults 75 + import androidx.compose.material3.Scaffold 76 + 77 + @Composable 78 + fun expressiveTextFieldColors(): TextFieldColors = TextFieldDefaults.colors( 79 + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, 80 + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, 81 + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer, 82 + focusedIndicatorColor = Color.Transparent, 83 + unfocusedIndicatorColor = Color.Transparent, 84 + disabledIndicatorColor = Color.Transparent, 85 + ) 86 + 87 + @Composable 88 + fun ExpressiveTextField( 89 + value: String, 90 + onValueChange: (String) -> Unit, 91 + modifier: Modifier = Modifier, 92 + enabled: Boolean = true, 93 + readOnly: Boolean = false, 94 + label: @Composable (() -> Unit)? = null, 95 + placeholder: @Composable (() -> Unit)? = null, 96 + leadingIcon: @Composable (() -> Unit)? = null, 97 + trailingIcon: @Composable (() -> Unit)? = null, 98 + supportingText: @Composable (() -> Unit)? = null, 99 + singleLine: Boolean = false, 100 + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 101 + visualTransformation: VisualTransformation = VisualTransformation.None, 102 + ) { 103 + TextField( 104 + value = value, 105 + onValueChange = onValueChange, 106 + modifier = modifier, 107 + enabled = enabled, 108 + readOnly = readOnly, 109 + label = label, 110 + placeholder = placeholder, 111 + leadingIcon = leadingIcon, 112 + trailingIcon = trailingIcon, 113 + supportingText = supportingText, 114 + singleLine = singleLine, 115 + maxLines = maxLines, 116 + visualTransformation = visualTransformation, 117 + shape = MaterialTheme.shapes.extraLarge, 118 + colors = expressiveTextFieldColors(), 119 + ) 120 + } 121 + 122 + object ExpressiveMotion { 123 + val Spatial: AnimationSpec<Float> = spring( 124 + dampingRatio = Spring.DampingRatioMediumBouncy, 125 + stiffness = Spring.StiffnessMediumLow, 126 + ) 127 + 128 + val Effects: AnimationSpec<Float> = tween(durationMillis = 220) 129 + } 130 + 131 + @Composable 132 + fun Modifier.expressivePressScale(interactionSource: MutableInteractionSource): Modifier { 133 + val pressed by interactionSource.collectIsPressedAsState() 134 + val scale by androidx.compose.animation.core.animateFloatAsState( 135 + targetValue = if (pressed) 0.96f else 1f, 136 + animationSpec = ExpressiveMotion.Spatial, 137 + label = "expressive-press-scale", 138 + ) 139 + return scale(scale) 140 + } 141 + 142 + @Composable 143 + fun ExpressiveButton( 144 + onClick: () -> Unit, 145 + modifier: Modifier = Modifier, 146 + enabled: Boolean = true, 147 + colors: ButtonColors = ButtonDefaults.buttonColors(), 148 + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 149 + content: @Composable RowScope.() -> Unit, 150 + ) { 151 + val interactionSource = remember { MutableInteractionSource() } 152 + Button( 153 + onClick = onClick, 154 + modifier = modifier.expressivePressScale(interactionSource), 155 + enabled = enabled, 156 + shape = MaterialTheme.shapes.extraLarge, 157 + colors = colors, 158 + contentPadding = contentPadding, 159 + interactionSource = interactionSource, 160 + content = content, 161 + ) 162 + } 163 + 164 + @Composable 165 + fun ExpressiveOutlinedButton( 166 + onClick: () -> Unit, 167 + modifier: Modifier = Modifier, 168 + enabled: Boolean = true, 169 + colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), 170 + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 171 + content: @Composable RowScope.() -> Unit, 172 + ) { 173 + val interactionSource = remember { MutableInteractionSource() } 174 + OutlinedButton( 175 + onClick = onClick, 176 + modifier = modifier.expressivePressScale(interactionSource), 177 + enabled = enabled, 178 + shape = MaterialTheme.shapes.extraLarge, 179 + colors = colors, 180 + contentPadding = contentPadding, 181 + interactionSource = interactionSource, 182 + content = content, 183 + ) 184 + } 185 + 186 + @Composable 187 + fun ExpressiveTextButton( 188 + onClick: () -> Unit, 189 + modifier: Modifier = Modifier, 190 + enabled: Boolean = true, 191 + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, 192 + content: @Composable RowScope.() -> Unit, 193 + ) { 194 + val interactionSource = remember { MutableInteractionSource() } 195 + TextButton( 196 + onClick = onClick, 197 + modifier = modifier.expressivePressScale(interactionSource), 198 + enabled = enabled, 199 + shape = MaterialTheme.shapes.extraLarge, 200 + contentPadding = contentPadding, 201 + interactionSource = interactionSource, 202 + content = content, 203 + ) 204 + } 205 + 206 + @Composable 207 + fun ExpressiveSwitch( 208 + checked: Boolean, 209 + onCheckedChange: ((Boolean) -> Unit)?, 210 + modifier: Modifier = Modifier, 211 + enabled: Boolean = true, 212 + ) { 213 + val interactionSource = remember { MutableInteractionSource() } 214 + Switch( 215 + checked = checked, 216 + onCheckedChange = onCheckedChange, 217 + modifier = modifier.expressivePressScale(interactionSource), 218 + enabled = enabled, 219 + colors = SwitchDefaults.colors( 220 + checkedTrackColor = MaterialTheme.colorScheme.primary, 221 + checkedThumbColor = MaterialTheme.colorScheme.onPrimary, 222 + checkedBorderColor = MaterialTheme.colorScheme.primary, 223 + uncheckedTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, 224 + uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant, 225 + uncheckedBorderColor = MaterialTheme.colorScheme.outlineVariant, 226 + ), 227 + interactionSource = interactionSource, 228 + ) 229 + } 230 + 231 + @Composable 232 + fun ExpressiveCheckbox( 233 + checked: Boolean, 234 + onCheckedChange: ((Boolean) -> Unit)?, 235 + modifier: Modifier = Modifier, 236 + enabled: Boolean = true, 237 + ) { 238 + val interactionSource = remember { MutableInteractionSource() } 239 + Checkbox( 240 + checked = checked, 241 + onCheckedChange = onCheckedChange, 242 + modifier = modifier.expressivePressScale(interactionSource), 243 + enabled = enabled, 244 + colors = CheckboxDefaults.colors( 245 + checkedColor = MaterialTheme.colorScheme.primary, 246 + uncheckedColor = MaterialTheme.colorScheme.outline, 247 + checkmarkColor = MaterialTheme.colorScheme.onPrimary, 248 + ), 249 + interactionSource = interactionSource, 250 + ) 251 + } 252 + 253 + @Composable 254 + fun ExpressiveFilterChip( 255 + selected: Boolean, 256 + onClick: () -> Unit, 257 + label: @Composable () -> Unit, 258 + modifier: Modifier = Modifier, 259 + enabled: Boolean = true, 260 + selectedColor: Color = MaterialTheme.colorScheme.primary, 261 + shape: Shape = MaterialTheme.shapes.extraLarge, 262 + ) { 263 + val interactionSource = remember { MutableInteractionSource() } 264 + FilterChip( 265 + selected = selected, 266 + onClick = onClick, 267 + label = label, 268 + modifier = modifier.expressivePressScale(interactionSource), 269 + enabled = enabled, 270 + shape = shape, 271 + colors = FilterChipDefaults.filterChipColors( 272 + containerColor = Color.White, 273 + labelColor = MaterialTheme.colorScheme.onSurface, 274 + selectedContainerColor = selectedColor, 275 + selectedLabelColor = MaterialTheme.colorScheme.onPrimary, 276 + ), 277 + border = FilterChipDefaults.filterChipBorder( 278 + enabled = enabled, 279 + selected = selected, 280 + borderColor = Color.Transparent, 281 + selectedBorderColor = selectedColor, 282 + ), 283 + interactionSource = interactionSource, 284 + ) 285 + } 286 + 287 + @Composable 288 + fun ExpressiveAssistChip( 289 + onClick: () -> Unit, 290 + label: @Composable () -> Unit, 291 + modifier: Modifier = Modifier, 292 + enabled: Boolean = true, 293 + containerColor: Color = MaterialTheme.colorScheme.secondaryContainer, 294 + labelColor: Color = MaterialTheme.colorScheme.onSecondaryContainer, 295 + ) { 296 + val interactionSource = remember { MutableInteractionSource() } 297 + AssistChip( 298 + onClick = onClick, 299 + label = label, 300 + modifier = modifier.expressivePressScale(interactionSource), 301 + enabled = enabled, 302 + shape = MaterialTheme.shapes.extraLarge, 303 + colors = AssistChipDefaults.assistChipColors( 304 + containerColor = containerColor, 305 + labelColor = labelColor, 306 + ), 307 + border = AssistChipDefaults.assistChipBorder(enabled = enabled, borderColor = Color.Transparent), 308 + interactionSource = interactionSource, 309 + ) 310 + } 311 + 312 + @Composable 313 + fun ExpressiveSnackbar( 314 + modifier: Modifier = Modifier, 315 + action: @Composable (() -> Unit)? = null, 316 + content: @Composable () -> Unit, 317 + ) { 318 + Snackbar( 319 + modifier = modifier, 320 + shape = MaterialTheme.shapes.extraLarge, 321 + containerColor = MaterialTheme.colorScheme.inverseSurface, 322 + contentColor = MaterialTheme.colorScheme.inverseOnSurface, 323 + actionContentColor = MaterialTheme.colorScheme.inversePrimary, 324 + action = action, 325 + content = content, 326 + ) 327 + } 328 + 329 + @Composable 330 + fun ExpressiveFloatingActionButton( 331 + onClick: () -> Unit, 332 + modifier: Modifier = Modifier, 333 + containerColor: Color = MaterialTheme.colorScheme.primary, 334 + contentColor: Color = MaterialTheme.colorScheme.onPrimary, 335 + content: @Composable () -> Unit, 336 + ) { 337 + val interactionSource = remember { MutableInteractionSource() } 338 + FloatingActionButton( 339 + onClick = onClick, 340 + modifier = modifier.expressivePressScale(interactionSource), 341 + shape = MaterialTheme.shapes.extraLarge, 342 + containerColor = containerColor, 343 + contentColor = contentColor, 344 + interactionSource = interactionSource, 345 + content = content, 346 + ) 347 + } 348 + 349 + @OptIn(ExperimentalMaterial3Api::class) 350 + @Composable 351 + fun ExpressiveSearchBar( 352 + inputField: @Composable () -> Unit, 353 + expanded: Boolean, 354 + onExpandedChange: (Boolean) -> Unit, 355 + modifier: Modifier = Modifier, 356 + colors: SearchBarColors = SearchBarDefaults.colors( 357 + containerColor = Color.White, 358 + dividerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.22f), 359 + ), 360 + content: @Composable ColumnScope.() -> Unit = {}, 361 + ) { 362 + SearchBar( 363 + inputField = inputField, 364 + expanded = expanded, 365 + onExpandedChange = onExpandedChange, 366 + modifier = modifier, 367 + shape = MaterialTheme.shapes.extraLarge, 368 + colors = colors, 369 + content = content, 370 + ) 371 + } 372 + 373 + @Composable 374 + fun ExpressiveDropdownMenu( 375 + expanded: Boolean, 376 + onDismissRequest: () -> Unit, 377 + modifier: Modifier = Modifier, 378 + content: @Composable ColumnScope.() -> Unit, 379 + ) { 380 + DropdownMenu( 381 + expanded = expanded, 382 + onDismissRequest = onDismissRequest, 383 + modifier = modifier, 384 + shape = MenuDefaults.shape, 385 + containerColor = MaterialTheme.colorScheme.surfaceContainer, 386 + tonalElevation = 0.dp, 387 + shadowElevation = 0.dp, 388 + content = content, 389 + ) 390 + } 391 + 392 + @OptIn(ExperimentalMaterial3Api::class) 393 + @Composable 394 + fun ExpressiveDropdownMenuItem( 395 + selected: Boolean, 396 + index: Int, 397 + lastIndex: Int, 398 + onClick: () -> Unit, 399 + text: @Composable () -> Unit, 400 + leadingIcon: @Composable (() -> Unit)? = null, 401 + ) { 402 + DropdownMenuItem( 403 + selected = selected, 404 + text = text, 405 + leadingIcon = leadingIcon, 406 + shapes = MenuDefaults.itemShape(index, lastIndex), 407 + colors = MenuDefaults.selectableItemVibrantColors(), 408 + onClick = onClick, 409 + ) 410 + } 411 + 412 + @Composable 413 + fun ExpressiveAlertDialog( 414 + onDismissRequest: () -> Unit, 415 + confirmButton: @Composable () -> Unit, 416 + modifier: Modifier = Modifier, 417 + dismissButton: @Composable (() -> Unit)? = null, 418 + icon: @Composable (() -> Unit)? = null, 419 + title: @Composable (() -> Unit)? = null, 420 + text: @Composable (() -> Unit)? = null, 421 + ) { 422 + AlertDialog( 423 + onDismissRequest = onDismissRequest, 424 + confirmButton = confirmButton, 425 + modifier = modifier, 426 + dismissButton = dismissButton, 427 + icon = icon, 428 + title = title, 429 + text = text, 430 + shape = MaterialTheme.shapes.extraLarge, 431 + containerColor = Color.White, 432 + tonalElevation = 0.dp, 433 + ) 434 + } 435 + 436 + @Composable 437 + fun ExpressiveNavigationBar( 438 + modifier: Modifier = Modifier, 439 + containerColor: Color, 440 + contentColor: Color, 441 + tonalElevation: Dp = 10.dp, 442 + content: @Composable RowScope.() -> Unit, 443 + ) { 444 + NavigationBar( 445 + modifier = modifier, 446 + containerColor = containerColor, 447 + contentColor = contentColor, 448 + tonalElevation = tonalElevation, 449 + content = content, 450 + ) 451 + } 452 + 453 + @Composable 454 + fun expressiveNavigationBarItemColors( 455 + contentColor: Color, 456 + selectedContainerColor: Color, 457 + ): NavigationBarItemColors = NavigationBarItemDefaults.colors( 458 + indicatorColor = selectedContainerColor, 459 + selectedIconColor = contentColor, 460 + selectedTextColor = contentColor, 461 + unselectedIconColor = contentColor.copy(alpha = 0.6f), 462 + unselectedTextColor = contentColor.copy(alpha = 0.6f), 463 + ) 464 + 465 + @Composable 466 + fun RowScope.ExpressiveNavigationBarItem( 467 + selected: Boolean, 468 + onClick: () -> Unit, 469 + icon: @Composable () -> Unit, 470 + modifier: Modifier = Modifier, 471 + enabled: Boolean = true, 472 + label: @Composable (() -> Unit)? = null, 473 + alwaysShowLabel: Boolean = true, 474 + colors: NavigationBarItemColors, 475 + ) { 476 + NavigationBarItem( 477 + selected = selected, 478 + onClick = onClick, 479 + icon = icon, 480 + modifier = modifier, 481 + enabled = enabled, 482 + label = label, 483 + alwaysShowLabel = alwaysShowLabel, 484 + colors = colors, 485 + ) 486 + } 487 + 488 + @Composable 489 + fun ExpressiveScaffold( 490 + modifier: Modifier = Modifier, 491 + topBar: @Composable () -> Unit = {}, 492 + bottomBar: @Composable () -> Unit = {}, 493 + snackbarHost: @Composable () -> Unit = {}, 494 + floatingActionButton: @Composable () -> Unit = {}, 495 + floatingActionButtonPosition: FabPosition = FabPosition.End, 496 + containerColor: Color = MaterialTheme.colorScheme.background, 497 + contentColor: Color = MaterialTheme.colorScheme.onBackground, 498 + contentWindowInsets: WindowInsets = WindowInsets.safeDrawing, 499 + content: @Composable (PaddingValues) -> Unit, 500 + ) { 501 + Scaffold( 502 + modifier = modifier, 503 + topBar = topBar, 504 + bottomBar = bottomBar, 505 + snackbarHost = snackbarHost, 506 + floatingActionButton = floatingActionButton, 507 + floatingActionButtonPosition = floatingActionButtonPosition, 508 + containerColor = containerColor, 509 + contentColor = contentColor, 510 + contentWindowInsets = contentWindowInsets, 511 + content = content, 512 + ) 513 + } 514 + 515 + @Composable 516 + fun ExpressiveBadge( 517 + modifier: Modifier = Modifier, 518 + containerColor: Color = MaterialTheme.colorScheme.error, 519 + contentColor: Color = MaterialTheme.colorScheme.onError, 520 + content: @Composable (RowScope.() -> Unit)? = null, 521 + ) { 522 + Badge( 523 + modifier = modifier, 524 + containerColor = containerColor, 525 + contentColor = contentColor, 526 + content = content, 527 + ) 528 + } 529 + 530 + @Composable 531 + fun ExpressiveBadgedBox( 532 + badge: @Composable BoxScope.() -> Unit, 533 + modifier: Modifier = Modifier, 534 + content: @Composable BoxScope.() -> Unit, 535 + ) { 536 + BadgedBox( 537 + badge = badge, 538 + modifier = modifier, 539 + content = content, 540 + ) 541 + } 542 + 543 + @Composable 544 + fun ExpressiveHorizontalDivider( 545 + modifier: Modifier = Modifier, 546 + color: Color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f), 547 + thickness: Dp = 1.dp, 548 + ) { 549 + HorizontalDivider( 550 + modifier = modifier, 551 + thickness = thickness, 552 + color = color, 553 + ) 554 + } 555 + 556 + @OptIn(ExperimentalMaterial3Api::class) 557 + @Composable 558 + fun ExpressivePullToRefreshBox( 559 + isRefreshing: Boolean, 560 + onRefresh: () -> Unit, 561 + modifier: Modifier = Modifier, 562 + content: @Composable BoxScope.() -> Unit, 563 + ) { 564 + PullToRefreshBox( 565 + isRefreshing = isRefreshing, 566 + onRefresh = onRefresh, 567 + modifier = modifier, 568 + content = content, 569 + ) 570 + } 571 + 572 + @Composable 573 + fun ExpressiveFilledTonalIconButton( 574 + onClick: () -> Unit, 575 + modifier: Modifier = Modifier, 576 + enabled: Boolean = true, 577 + content: @Composable () -> Unit, 578 + ) { 579 + val interactionSource = remember { MutableInteractionSource() } 580 + FilledTonalIconButton( 581 + onClick = onClick, 582 + modifier = modifier.expressivePressScale(interactionSource), 583 + enabled = enabled, 584 + interactionSource = interactionSource, 585 + content = content, 586 + ) 587 + } 588 + 589 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 590 + @Composable 591 + fun ExpressiveToggleButton( 592 + checked: Boolean, 593 + onCheckedChange: (Boolean) -> Unit, 594 + modifier: Modifier = Modifier, 595 + enabled: Boolean = true, 596 + shapes: ToggleButtonShapes, 597 + content: @Composable RowScope.() -> Unit, 598 + ) { 599 + ToggleButton( 600 + checked = checked, 601 + onCheckedChange = onCheckedChange, 602 + modifier = modifier, 603 + enabled = enabled, 604 + shapes = shapes, 605 + content = content, 606 + ) 607 + } 608 + 609 + @Composable 610 + fun ExpressiveLoadingIndicator( 611 + modifier: Modifier = Modifier, 612 + color: Color = MaterialTheme.colorScheme.primary, 613 + size: Dp = 48.dp, 614 + ) { 615 + CircularProgressIndicator( 616 + modifier = modifier.size(size), 617 + color = color, 618 + trackColor = color.copy(alpha = 0.18f), 619 + strokeWidth = 5.dp, 620 + gapSize = 3.dp, 621 + ) 622 + } 623 + 624 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 625 + @Composable 626 + fun ExpressiveTopLoadingIndicator( 627 + modifier: Modifier = Modifier, 628 + color: Color = MaterialTheme.colorScheme.primary, 629 + trackColor: Color = color.copy(alpha = 0.18f), 630 + ) { 631 + LinearWavyProgressIndicator( 632 + modifier = modifier.fillMaxWidth().height(18.dp), 633 + color = color, 634 + trackColor = trackColor, 635 + stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke, 636 + trackStroke = WavyProgressIndicatorDefaults.linearTrackStroke, 637 + wavelength = WavyProgressIndicatorDefaults.LinearIndeterminateWavelength, 638 + waveSpeed = 12.dp, 639 + ) 640 + } 641 + 642 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 643 + @Composable 644 + fun ExpressiveTopProgressIndicator( 645 + progress: () -> Float, 646 + modifier: Modifier = Modifier, 647 + color: Color = MaterialTheme.colorScheme.primary, 648 + trackColor: Color = color.copy(alpha = 0.18f), 649 + ) { 650 + LinearWavyProgressIndicator( 651 + progress = progress, 652 + modifier = modifier.fillMaxWidth().height(18.dp), 653 + color = color, 654 + trackColor = trackColor, 655 + stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke, 656 + trackStroke = WavyProgressIndicatorDefaults.linearTrackStroke, 657 + wavelength = WavyProgressIndicatorDefaults.LinearDeterminateWavelength, 658 + amplitude = WavyProgressIndicatorDefaults.indicatorAmplitude, 659 + stopSize = WavyProgressIndicatorDefaults.LinearTrackStopIndicatorSize, 660 + gapSize = WavyProgressIndicatorDefaults.LinearIndicatorTrackGapSize, 661 + ) 662 + } 663 + 664 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 665 + @Composable 666 + fun ExpressiveLinearProgressIndicator( 667 + progress: () -> Float, 668 + modifier: Modifier = Modifier, 669 + color: Color = MaterialTheme.colorScheme.primary, 670 + trackColor: Color = color.copy(alpha = 0.18f), 671 + ) { 672 + LinearWavyProgressIndicator( 673 + progress = progress, 674 + modifier = modifier, 675 + color = color, 676 + trackColor = trackColor, 677 + stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke, 678 + trackStroke = WavyProgressIndicatorDefaults.linearTrackStroke, 679 + wavelength = WavyProgressIndicatorDefaults.LinearDeterminateWavelength, 680 + amplitude = { WavyProgressIndicatorDefaults.indicatorAmplitude(it) * 0.45f }, 681 + stopSize = WavyProgressIndicatorDefaults.LinearTrackStopIndicatorSize, 682 + gapSize = WavyProgressIndicatorDefaults.LinearIndicatorTrackGapSize, 683 + ) 684 + } 685 + 686 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 687 + @Composable 688 + fun ExpressiveLinearProgressIndicator( 689 + modifier: Modifier = Modifier, 690 + color: Color = MaterialTheme.colorScheme.primary, 691 + trackColor: Color = color.copy(alpha = 0.18f), 692 + ) { 693 + LinearWavyProgressIndicator( 694 + modifier = modifier, 695 + color = color, 696 + trackColor = trackColor, 697 + stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke, 698 + trackStroke = WavyProgressIndicatorDefaults.linearTrackStroke, 699 + wavelength = WavyProgressIndicatorDefaults.LinearIndeterminateWavelength, 700 + waveSpeed = 8.dp, 701 + ) 702 + } 703 + 704 + @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) 705 + @Composable 706 + fun ExpressiveSlider( 707 + value: Float, 708 + onValueChange: (Float) -> Unit, 709 + modifier: Modifier = Modifier, 710 + enabled: Boolean = true, 711 + valueRange: ClosedFloatingPointRange<Float> = 0f..1f, 712 + steps: Int = 0, 713 + ) { 714 + val interactionSource = remember { MutableInteractionSource() } 715 + val colors = SliderDefaults.colors( 716 + thumbColor = MaterialTheme.colorScheme.secondary, 717 + activeTrackColor = MaterialTheme.colorScheme.secondary, 718 + inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.18f), 719 + activeTickColor = MaterialTheme.colorScheme.onSecondary, 720 + inactiveTickColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.38f), 721 + ) 722 + Slider( 723 + value = value, 724 + onValueChange = onValueChange, 725 + modifier = modifier, 726 + enabled = enabled, 727 + valueRange = valueRange, 728 + steps = steps, 729 + colors = colors, 730 + interactionSource = interactionSource, 731 + thumb = { sliderState -> 732 + SliderDefaults.Thumb( 733 + interactionSource = interactionSource, 734 + sliderState = sliderState, 735 + colors = colors, 736 + enabled = enabled, 737 + thumbSize = DpSize(6.dp, 34.dp), 738 + ) 739 + }, 740 + track = { sliderState -> 741 + val stopColor = MaterialTheme.colorScheme.secondary 742 + SliderDefaults.Track( 743 + sliderState = sliderState, 744 + colors = colors, 745 + enabled = enabled, 746 + trackInsideCornerSize = 8.dp, 747 + thumbTrackGapSize = 6.dp, 748 + drawStopIndicator = { offset -> 749 + drawCircle( 750 + color = stopColor, 751 + radius = 4.dp.toPx(), 752 + center = offset, 753 + ) 754 + }, 755 + ) 756 + }, 757 + ) 758 + }
+95
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/ExpressiveSurfaces.kt
··· 1 + package com.derakkuma.ui.components 2 + 3 + import androidx.compose.foundation.BorderStroke 4 + import androidx.compose.foundation.background 5 + import androidx.compose.foundation.border 6 + import androidx.compose.foundation.layout.Box 7 + import androidx.compose.foundation.layout.BoxScope 8 + import androidx.compose.foundation.layout.size 9 + import androidx.compose.foundation.layout.padding 10 + import androidx.compose.foundation.shape.RoundedCornerShape 11 + import androidx.compose.material3.MaterialTheme 12 + import androidx.compose.material3.Surface 13 + import androidx.compose.runtime.Composable 14 + import androidx.compose.ui.Modifier 15 + import androidx.compose.ui.draw.clip 16 + import androidx.compose.ui.graphics.Color 17 + import androidx.compose.ui.graphics.Shape 18 + import androidx.compose.ui.unit.Dp 19 + import androidx.compose.ui.unit.dp 20 + 21 + @Composable 22 + fun Modifier.expressiveScreenBackground(): Modifier = 23 + background(MaterialTheme.colorScheme.background) 24 + 25 + @Composable 26 + fun ExpressiveGlassCard( 27 + modifier: Modifier = Modifier, 28 + cornerRadius: Dp = 28.dp, 29 + containerColor: Color = Color.White, 30 + content: @Composable BoxScope.() -> Unit, 31 + ) { 32 + val shape = RoundedCornerShape(cornerRadius) 33 + Surface( 34 + modifier = modifier, 35 + shape = shape, 36 + color = containerColor, 37 + tonalElevation = 0.dp, 38 + shadowElevation = 0.dp, 39 + ) { 40 + Box( 41 + modifier = Modifier 42 + .clip(shape) 43 + .background(containerColor) 44 + .border(BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f)), shape), 45 + content = content, 46 + ) 47 + } 48 + } 49 + 50 + @Composable 51 + fun ExpressiveIconSurface( 52 + modifier: Modifier = Modifier, 53 + size: Dp = 40.dp, 54 + shape: Shape = MaterialTheme.shapes.extraLarge, 55 + containerColor: Color = MaterialTheme.colorScheme.primaryContainer, 56 + content: @Composable BoxScope.() -> Unit, 57 + ) { 58 + Box( 59 + modifier = modifier 60 + .size(size) 61 + .clip(shape) 62 + .background(containerColor), 63 + content = content, 64 + ) 65 + } 66 + 67 + @Composable 68 + fun ExpressiveBadgeSurface( 69 + modifier: Modifier = Modifier, 70 + shape: Shape = MaterialTheme.shapes.extraLarge, 71 + containerColor: Color, 72 + content: @Composable BoxScope.() -> Unit, 73 + ) { 74 + Box( 75 + modifier = modifier 76 + .clip(shape) 77 + .background(containerColor), 78 + content = content, 79 + ) 80 + } 81 + 82 + @Composable 83 + fun ExpressiveDot( 84 + modifier: Modifier = Modifier, 85 + size: Dp = 10.dp, 86 + color: Color, 87 + ) { 88 + Box( 89 + modifier = modifier 90 + .size(size) 91 + .clip(MaterialTheme.shapes.extraLarge) 92 + .background(color) 93 + .padding(0.dp), 94 + ) 95 + }
+24 -13
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/LoadingContent.kt
··· 40 40 @Composable 41 41 fun BigStat(value: String, label: String) { 42 42 Column(horizontalAlignment = Alignment.CenterHorizontally) { 43 - Text(value, fontSize = 28.sp, fontWeight = FontWeight.Bold, color = DarkText) 44 - Text(label, fontSize = 12.sp, color = MutedText) 43 + Text(value, fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 44 + Text(label, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 45 45 } 46 46 } 47 47 48 48 @Composable 49 49 fun LoadingState(modifier: Modifier = Modifier.fillMaxWidth().padding(top = 60.dp), color: Color = MaterialTheme.colorScheme.onBackground) { 50 50 Box(modifier, contentAlignment = Alignment.Center) { 51 - CircularProgressIndicator(color = color) 51 + ExpressiveLoadingIndicator(color = color) 52 52 } 53 53 } 54 54 ··· 61 61 ) { 62 62 Text(error, fontSize = 14.sp, color = MaterialTheme.colorScheme.error, fontWeight = FontWeight.Medium) 63 63 if (onRetry != null) { 64 - TextButton(onClick = onRetry) { 65 - Text("Retry", color = PrimaryBlue, fontWeight = FontWeight.SemiBold) 64 + ExpressiveButton( 65 + onClick = onRetry, 66 + colors = ButtonDefaults.filledTonalButtonColors( 67 + containerColor = MaterialTheme.colorScheme.errorContainer, 68 + contentColor = MaterialTheme.colorScheme.onErrorContainer, 69 + ), 70 + ) { 71 + Text("Retry", fontWeight = FontWeight.Bold) 66 72 } 67 73 } 68 74 } ··· 71 77 @Composable 72 78 fun EmptyState(message: String, modifier: Modifier = Modifier.fillMaxWidth().padding(24.dp)) { 73 79 Box(modifier, contentAlignment = Alignment.Center) { 74 - Text(message, color = MutedText, fontWeight = FontWeight.Medium) 80 + Text(message, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Medium) 75 81 } 76 82 } 77 83 ··· 84 90 85 91 @Composable 86 92 fun EmptyCard(text: String, modifier: Modifier = Modifier.fillMaxWidth()) { 87 - InfoCard(text, MutedText, modifier) 93 + InfoCard(text, MaterialTheme.colorScheme.onSurfaceVariant, modifier) 88 94 } 89 95 90 96 @Composable 91 97 fun SectionHeader(text: String, modifier: Modifier = Modifier.padding(top = 8.dp, start = 4.dp)) { 92 - Text(text, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = modifier) 98 + Text( 99 + text, 100 + style = MaterialTheme.typography.titleSmall, 101 + color = MaterialTheme.colorScheme.onSurface, 102 + modifier = modifier, 103 + ) 93 104 } 94 105 95 106 @Composable 96 107 fun DerakkumaCard( 97 108 modifier: Modifier = Modifier.fillMaxWidth(), 98 - containerColor: Color = CardWhite, 99 - cornerRadius: Dp = 8.dp, 109 + containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, 110 + cornerRadius: Dp = 24.dp, 100 111 content: @Composable () -> Unit, 101 112 ) { 102 - Card( 113 + ExpressiveGlassCard( 103 114 modifier = modifier, 104 - shape = androidx.compose.foundation.shape.RoundedCornerShape(cornerRadius), 105 - colors = CardDefaults.cardColors(containerColor = containerColor), 115 + cornerRadius = cornerRadius, 116 + containerColor = containerColor, 106 117 ) { 107 118 content() 108 119 }
+12 -18
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/MaimaiProfile.kt
··· 19 19 import androidx.compose.foundation.layout.width 20 20 import androidx.compose.foundation.layout.widthIn 21 21 import androidx.compose.foundation.shape.RoundedCornerShape 22 - import androidx.compose.material3.Card 23 - import androidx.compose.material3.CardDefaults 22 + import androidx.compose.material3.MaterialTheme 24 23 import androidx.compose.material3.Text 25 24 import androidx.compose.runtime.Composable 26 25 import androidx.compose.ui.Alignment ··· 43 42 import com.derakkuma.ui.images.ratingPlateImage 44 43 import com.derakkuma.ui.images.rememberBundledImage 45 44 import com.derakkuma.ui.images.trophyPlateImage 46 - import com.derakkuma.ui.theme.CardWhite 47 - import com.derakkuma.ui.theme.DarkText 48 - import com.derakkuma.ui.theme.MutedText 49 - import com.derakkuma.ui.theme.PrimaryBlue 50 45 51 46 @Composable 52 47 fun MaimaiProfile( ··· 72 67 details: @Composable ColumnScope.() -> Unit = {}, 73 68 actions: @Composable RowScope.() -> Unit = {}, 74 69 ) { 75 - Card( 70 + ExpressiveGlassCard( 76 71 modifier = modifier 77 72 .fillMaxWidth() 78 73 .then(if (onClick != null) Modifier.clickable { onClick() } else Modifier), 79 - shape = RoundedCornerShape(8.dp), 80 - colors = CardDefaults.cardColors(containerColor = CardWhite), 74 + cornerRadius = 24.dp, 81 75 ) { 82 76 Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 83 77 Row(verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(10.dp)) { ··· 105 99 "×$stars", 106 100 fontSize = ratingTextSize, 107 101 fontWeight = if (ratingPlate != null) FontWeight.SemiBold else FontWeight.Normal, 108 - color = if (ratingPlate != null) DarkText else MutedText, 102 + color = if (ratingPlate != null) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, 109 103 ) 110 104 } 111 105 ratingTrailing() ··· 118 112 } 119 113 120 114 if (showComment && comment.isNotBlank()) { 121 - Text(comment, fontSize = 12.sp, color = MutedText) 115 + Text(comment, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 122 116 } 123 117 124 118 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { ··· 135 129 fontSize: TextUnit, 136 130 ) { 137 131 if (ratingPlate == null) { 138 - Text("Rating $rating", fontSize = fontSize, fontWeight = FontWeight.SemiBold, color = PrimaryBlue) 132 + Text("Rating $rating", fontSize = fontSize, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) 139 133 return 140 134 } 141 135 ··· 150 144 ) 151 145 } 152 146 Text( 153 - "$rating ", 147 + "$rating ", 154 148 fontSize = fontSize, 155 149 fontWeight = FontWeight.ExtraBold, 156 - color = Color.White, 150 + color = MaterialTheme.colorScheme.onPrimary, 157 151 ) 158 152 } 159 153 } ··· 177 171 ) { 178 172 MaimaiRemoteImage( 179 173 url = iconUrl, 180 - modifier = Modifier.size(iconSize).clip(RoundedCornerShape(6.dp)), 174 + modifier = Modifier.size(iconSize).clip(RoundedCornerShape(2.dp)), 181 175 contentDescription = "Profile icon", 182 176 contentScale = ContentScale.Crop, 183 177 ) ··· 188 182 modifier = Modifier.weight(1f), 189 183 verticalArrangement = Arrangement.spacedBy(3.dp), 190 184 ) { 191 - Text(name, fontSize = nameFontSize, fontWeight = FontWeight.Bold, color = DarkText, maxLines = 1) 185 + Text(name, fontSize = nameFontSize, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, maxLines = 1) 192 186 if (title.isNotBlank()) { 193 187 MaimaiTrophyTitle(title = title, rarity = titleRarity) 194 188 } ··· 249 243 textAlign = TextAlign.Center, 250 244 style = TextStyle( 251 245 fontSize = 12.sp, 252 - color = Color.White, 246 + color = MaterialTheme.colorScheme.onPrimary, 253 247 shadow = Shadow( 254 - color = Color.Black, 248 + color = MaterialTheme.colorScheme.scrim, 255 249 blurRadius = 4f, 256 250 ), 257 251 ),
+2 -2
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/MaintenanceBanner.kt
··· 1 1 package com.derakkuma.ui.components 2 2 3 + import androidx.compose.material3.MaterialTheme 3 4 import androidx.compose.material3.Text 4 5 import androidx.compose.runtime.Composable 5 6 import androidx.compose.runtime.LaunchedEffect ··· 10 11 import androidx.compose.ui.text.font.FontWeight 11 12 import androidx.compose.ui.unit.sp 12 13 import com.derakkuma.data.MaintenanceMode 13 - import com.derakkuma.ui.theme.CardWhite 14 14 import kotlinx.coroutines.delay 15 15 16 16 @Composable ··· 38 38 countdown, 39 39 fontSize = 20.sp, 40 40 fontWeight = FontWeight.ExtraBold, 41 - color = CardWhite, 41 + color = MaterialTheme.colorScheme.onError, 42 42 ) 43 43 }, 44 44 )
+5 -5
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/MessageBanner.kt
··· 10 10 import androidx.compose.foundation.layout.padding 11 11 import androidx.compose.foundation.layout.size 12 12 import androidx.compose.foundation.layout.width 13 + import androidx.compose.material3.MaterialTheme 13 14 import androidx.compose.material3.Text 14 15 import androidx.compose.runtime.Composable 15 16 import androidx.compose.runtime.LaunchedEffect ··· 26 27 import androidx.compose.ui.unit.dp 27 28 import androidx.compose.ui.unit.sp 28 29 import com.derakkuma.ui.images.MaimaiAssets 29 - import com.derakkuma.ui.theme.CardWhite 30 - import com.derakkuma.ui.theme.DangerRed 31 30 32 31 @Composable 33 32 fun MessageBanner( 34 33 title: String, 35 34 message: String, 36 35 contentDescription: String, 37 - backgroundColor: Color = DangerRed.copy(alpha = 0.9f), 36 + backgroundColor: Color = MaterialTheme.colorScheme.error.copy(alpha = 0.9f), 38 37 modifier: Modifier = Modifier, 39 38 messageAlpha: Float = 0.75f, 40 39 fallbackIcon: @Composable (() -> Unit)? = null, 41 40 trailingContent: @Composable (() -> Unit)? = null, 42 41 ) { 43 42 var charaBitmap by remember { mutableStateOf<ImageBitmap?>(null) } 43 + val contentColor = MaterialTheme.colorScheme.onError 44 44 LaunchedEffect(Unit) { 45 45 charaBitmap = MaimaiAssets.load("chara_01.png") 46 46 } ··· 73 73 title, 74 74 fontSize = 14.sp, 75 75 fontWeight = FontWeight.Bold, 76 - color = CardWhite, 76 + color = contentColor, 77 77 ) 78 78 Text( 79 79 message, 80 80 fontSize = 11.sp, 81 - color = CardWhite.copy(alpha = messageAlpha), 81 + color = contentColor.copy(alpha = messageAlpha), 82 82 ) 83 83 } 84 84
+10 -12
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/PlayHeatmap.kt
··· 5 5 import androidx.compose.animation.fadeOut 6 6 import androidx.compose.foundation.layout.* 7 7 import androidx.compose.foundation.shape.RoundedCornerShape 8 - import androidx.compose.material3.Card 9 - import androidx.compose.material3.CardDefaults 10 - import androidx.compose.material3.Snackbar 8 + import androidx.compose.material3.MaterialTheme 11 9 import androidx.compose.material3.Text 12 10 import androidx.compose.runtime.* 13 11 import androidx.compose.ui.Alignment ··· 70 68 71 69 var toastText by remember { mutableStateOf<String?>(null) } 72 70 val scope = rememberCoroutineScope() 71 + val inactiveHeatColor = MaterialTheme.colorScheme.surfaceVariant 72 + val activeHeatColor = MaterialTheme.colorScheme.secondary 73 73 74 - Card( 74 + ExpressiveGlassCard( 75 75 modifier = modifier.fillMaxWidth(), 76 - shape = RoundedCornerShape(8.dp), 77 - colors = CardDefaults.cardColors(containerColor = CardWhite), 76 + cornerRadius = 24.dp, 78 77 ) { 79 78 Column(modifier = Modifier.padding(12.dp)) { 80 - Text("Play Activity", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 79 + Text("Play Activity", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 81 80 Spacer(Modifier.height(6.dp)) 82 81 83 82 HeatMap( ··· 86 85 heatStyle = HeatStyle( 87 86 height = 14.dp, 88 87 heatColor = HeatColor( 89 - inactiveColor = Color(0xFFE5E7EB), 90 - activeLowestColor = Color(0xFFE5E7EB), // same as inactive so 0-play days look gray 91 - activeHighestColor = Color(0xFFFF699D), 88 + inactiveColor = inactiveHeatColor, 89 + activeLowestColor = inactiveHeatColor, // same as inactive so 0-play days look gray 90 + activeHighestColor = activeHeatColor, 92 91 ), 93 92 heatShape = RoundedCornerShape(2.dp), 94 93 showHeatValue = false, ··· 118 117 ) { 119 118 Column { 120 119 Spacer(Modifier.height(6.dp)) 121 - Snackbar( 120 + ExpressiveSnackbar( 122 121 modifier = Modifier.fillMaxWidth(), 123 - shape = RoundedCornerShape(6.dp), 124 122 ) { 125 123 Text(toastText ?: "", fontSize = 12.sp) 126 124 }
+58 -47
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/SongRow.kt
··· 2 2 3 3 import androidx.compose.foundation.background 4 4 import androidx.compose.foundation.layout.* 5 - import androidx.compose.foundation.shape.RoundedCornerShape 5 + import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 6 + import androidx.compose.material3.MaterialShapes 7 + import androidx.compose.material3.MaterialTheme 6 8 import androidx.compose.material3.Text 9 + import androidx.compose.material3.toShape 7 10 import androidx.compose.runtime.Composable 8 11 import androidx.compose.ui.Alignment 9 12 import androidx.compose.ui.Modifier 10 13 import androidx.compose.ui.draw.clip 14 + import androidx.compose.ui.graphics.Color 11 15 import androidx.compose.ui.layout.ContentScale 12 16 import androidx.compose.ui.text.font.FontWeight 13 17 import androidx.compose.ui.text.style.TextOverflow ··· 24 28 * Reusable song row with album art, difficulty badge, name, badges, and score. 25 29 * Used in both Record (playlog) and Rating tabs. 26 30 */ 31 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 27 32 @Composable 28 33 fun SongRow( 29 34 name: String, ··· 43 48 datetime: String = "", 44 49 // Whether to use playlog-style rank images or music_icon style 45 50 usePlaylogRanks: Boolean = true, 51 + containerColor: Color = Color.Transparent, 46 52 ) { 47 53 Row( 48 - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), 54 + modifier = Modifier 55 + .fillMaxWidth() 56 + .clip(MaterialTheme.shapes.extraLarge) 57 + .background(containerColor) 58 + .padding(horizontal = 12.dp, vertical = 8.dp), 49 59 verticalAlignment = Alignment.CenterVertically, 60 + horizontalArrangement = Arrangement.spacedBy(12.dp), 50 61 ) { 51 - // Album art 52 - Box(modifier = Modifier.size(artSize)) { 53 - if (albumArtUrl != null) { 62 + // Album art 63 + Box(modifier = Modifier.size(artSize)) { 64 + if (albumArtUrl != null) { 65 + MaimaiRemoteImage( 66 + url = albumArtUrl, 67 + modifier = Modifier.fillMaxSize().clip(MaterialShapes.Square.toShape()), 68 + contentDescription = name, 69 + contentScale = ContentScale.Crop, 70 + ) 71 + } else { 72 + Box( 73 + modifier = Modifier.fillMaxSize() 74 + .clip(MaterialShapes.Square.toShape()) 75 + .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f)), 76 + ) 77 + } 78 + // Difficulty badge overlaid bottom-left 54 79 MaimaiRemoteImage( 55 - url = albumArtUrl, 56 - modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(4.dp)), 57 - contentDescription = name, 58 - contentScale = ContentScale.Crop, 59 - ) 60 - } else { 61 - Box( 62 - modifier = Modifier.fillMaxSize() 63 - .clip(RoundedCornerShape(4.dp)) 64 - .background(MutedText.copy(alpha = 0.1f)), 80 + url = "$IMG_BASE/diff_$difficulty.png", 81 + modifier = Modifier.height(14.dp).align(Alignment.BottomStart), 82 + contentDescription = difficulty, 65 83 ) 66 84 } 67 - // Difficulty badge overlaid bottom-left 68 - MaimaiRemoteImage( 69 - url = "$IMG_BASE/diff_$difficulty.png", 70 - modifier = Modifier.height(14.dp).align(Alignment.BottomStart), 71 - contentDescription = difficulty, 72 - ) 73 - } 74 85 75 - Spacer(Modifier.width(8.dp)) 76 - 77 - // Song info 78 - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { 86 + Column( 87 + modifier = Modifier.weight(1f), 88 + verticalArrangement = Arrangement.spacedBy(4.dp), 89 + ) { 79 90 // Title row 80 91 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { 81 92 Text( 82 93 name, 83 94 fontSize = 13.sp, 84 95 fontWeight = FontWeight.SemiBold, 85 - color = DarkText, 96 + color = MaterialTheme.colorScheme.onSurface, 86 97 maxLines = 1, 87 98 overflow = TextOverflow.Ellipsis, 88 99 modifier = Modifier.weight(1f, fill = false), ··· 128 139 if (placingImg != null) { 129 140 BundledIcon(image = placingImg, modifier = Modifier.height(14.dp), contentDescription = placing) 130 141 } else { 131 - Text(placing, fontSize = 10.sp, color = MutedText) 142 + Text(placing, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 132 143 } 133 144 } 134 145 } 135 146 } 136 147 137 - // Score + rank column 138 - Column(horizontalAlignment = Alignment.End) { 139 - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(3.dp)) { 140 - Text( 141 - "${formatScore(score)}%", 142 - fontSize = 13.sp, 143 - fontWeight = FontWeight.Bold, 144 - color = AccentPink, 145 - ) 146 - val rankIcon = if (usePlaylogRanks) scoreRankImage(scoreRank) else musicIconImage(scoreRank) 147 - if (rankIcon != null) { 148 - BundledIcon(image = rankIcon, modifier = Modifier.height(16.dp), contentDescription = scoreRank) 148 + // Score + rank column 149 + Column(horizontalAlignment = Alignment.End) { 150 + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(3.dp)) { 151 + Text( 152 + "${formatScore(score)}%", 153 + fontSize = 13.sp, 154 + fontWeight = FontWeight.Bold, 155 + color = MaterialTheme.colorScheme.secondary, 156 + ) 157 + val rankIcon = if (usePlaylogRanks) scoreRankImage(scoreRank) else musicIconImage(scoreRank) 158 + if (rankIcon != null) { 159 + BundledIcon(image = rankIcon, modifier = Modifier.height(16.dp), contentDescription = scoreRank) 160 + } 149 161 } 150 - } 151 - if (dxScore.isNotBlank()) { 152 - Text("DX $dxScore", fontSize = 10.sp, color = MutedText) 153 - } 154 - if (datetime.isNotBlank()) { 155 - Text(datetime, fontSize = 10.sp, color = MutedText) 162 + if (dxScore.isNotBlank()) { 163 + Text("DX $dxScore", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 164 + } 165 + if (datetime.isNotBlank()) { 166 + Text(datetime, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 167 + } 156 168 } 157 - } 158 169 } 159 170 }
+23 -39
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/SplitButtonRow.kt
··· 1 1 package com.derakkuma.ui.components 2 2 3 - import androidx.compose.foundation.background 4 - import androidx.compose.foundation.clickable 5 - import androidx.compose.foundation.layout.Box 6 3 import androidx.compose.foundation.layout.Column 7 4 import androidx.compose.foundation.layout.Row 8 - import androidx.compose.foundation.layout.defaultMinSize 9 - import androidx.compose.foundation.layout.fillMaxWidth 10 5 import androidx.compose.foundation.layout.padding 11 6 import androidx.compose.foundation.layout.size 12 - import androidx.compose.foundation.shape.RoundedCornerShape 7 + import androidx.compose.material3.ButtonGroupDefaults 8 + import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 13 9 import androidx.compose.material3.Icon 10 + import androidx.compose.material3.MaterialTheme 14 11 import androidx.compose.material3.Text 15 12 import androidx.compose.runtime.Composable 16 13 import androidx.compose.ui.Alignment 17 14 import androidx.compose.ui.Modifier 18 - import androidx.compose.ui.draw.clip 19 - import androidx.compose.ui.graphics.Color 20 15 import androidx.compose.ui.graphics.vector.ImageVector 21 16 import androidx.compose.ui.text.font.FontWeight 22 - import androidx.compose.ui.text.style.TextAlign 23 - import androidx.compose.ui.unit.Dp 24 17 import androidx.compose.ui.unit.dp 25 18 import androidx.compose.ui.unit.sp 26 - import com.derakkuma.ui.theme.CardWhite 27 - import com.derakkuma.ui.theme.DarkText 28 - import com.derakkuma.ui.theme.MutedText 29 - import com.derakkuma.ui.theme.PrimaryBlue 30 19 20 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 31 21 @Composable 32 22 fun SplitButtonRow( 33 23 label: String, ··· 38 28 description: String? = null, 39 29 disabled: Boolean = false, 40 30 icons: Map<Int, ImageVector>? = null, 41 - minOptionWidth: Dp = 44.dp, 42 - minOptionHeight: Dp = 38.dp, 43 - unselectedContainerColor: Color = MutedText.copy(alpha = 0.1f), 44 31 ) { 45 32 Row( 46 - modifier = modifier.fillMaxWidth().padding(vertical = 4.dp), 33 + modifier = modifier.padding(vertical = 4.dp), 47 34 verticalAlignment = Alignment.CenterVertically, 48 35 ) { 49 36 Column(modifier = Modifier.weight(1f)) { 50 - Text(label, fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 37 + Text(label, fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 51 38 if (!description.isNullOrBlank()) { 52 - Text(description, fontSize = 11.sp, color = MutedText) 39 + Text(description, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 53 40 } 54 41 } 55 42 56 - Row(modifier = Modifier.clip(RoundedCornerShape(6.dp))) { 43 + Row( 44 + horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy( 45 + ButtonGroupDefaults.ConnectedSpaceBetween, 46 + ), 47 + ) { 57 48 options.forEachIndexed { i, (value, optLabel) -> 58 49 val selectedOption = value == selected 59 - val bgColor = if (selectedOption) PrimaryBlue else unselectedContainerColor 60 - val textColor = if (selectedOption) CardWhite else DarkText 61 50 val icon = icons?.get(i) 62 - 63 - Box( 64 - modifier = Modifier 65 - .defaultMinSize(minWidth = minOptionWidth, minHeight = minOptionHeight) 66 - .background(if (disabled) bgColor.copy(alpha = 0.4f) else bgColor) 67 - .clickable(enabled = !disabled) { onSelect(value) } 68 - .padding(horizontal = 12.dp, vertical = 6.dp), 69 - contentAlignment = Alignment.Center, 51 + ExpressiveToggleButton( 52 + checked = selectedOption, 53 + onCheckedChange = { onSelect(value) }, 54 + enabled = !disabled, 55 + shapes = when (i) { 56 + 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() 57 + options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() 58 + else -> ButtonGroupDefaults.connectedMiddleButtonShapes() 59 + }, 70 60 ) { 71 61 if (icon != null) { 72 - Icon(icon, contentDescription = optLabel, modifier = Modifier.size(18.dp), tint = textColor) 62 + Icon(icon, contentDescription = optLabel, modifier = Modifier.size(18.dp)) 73 63 } else { 74 - Text( 75 - optLabel, 76 - fontSize = 12.sp, 77 - fontWeight = if (selectedOption) FontWeight.Bold else FontWeight.Medium, 78 - color = textColor, 79 - textAlign = TextAlign.Center, 80 - ) 64 + Text(optLabel) 81 65 } 82 66 } 83 67 }
+2 -2
composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/VerizonNetworkBanner.kt
··· 4 4 import androidx.compose.material.icons.Icons 5 5 import androidx.compose.material.icons.outlined.WifiOff 6 6 import androidx.compose.material3.Icon 7 + import androidx.compose.material3.MaterialTheme 7 8 import androidx.compose.runtime.Composable 8 9 import androidx.compose.ui.Modifier 9 10 import androidx.compose.ui.unit.dp 10 - import com.derakkuma.ui.theme.CardWhite 11 11 12 12 @Composable 13 13 fun VerizonNetworkBanner() { ··· 21 21 imageVector = Icons.Outlined.WifiOff, 22 22 contentDescription = "Verizon network warning", 23 23 modifier = Modifier.size(40.dp), 24 - tint = CardWhite, 24 + tint = MaterialTheme.colorScheme.onError, 25 25 ) 26 26 }, 27 27 )
+26 -18
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/AppIconTab.kt
··· 7 7 import androidx.compose.foundation.lazy.grid.GridCells 8 8 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 9 9 import androidx.compose.foundation.lazy.grid.items 10 - import androidx.compose.foundation.shape.RoundedCornerShape 11 10 import androidx.compose.material3.* 12 11 import androidx.compose.runtime.* 13 12 import androidx.compose.ui.Alignment ··· 31 30 import com.derakkuma.atproto.openOAuthUrl 32 31 import com.derakkuma.data.CacheStore 33 32 import com.derakkuma.donation.DonationUnlock 33 + import com.derakkuma.ui.components.ExpressiveGlassCard 34 + import com.derakkuma.ui.components.ExpressiveTextButton 35 + import com.derakkuma.ui.components.ExpressiveAlertDialog 36 + import com.derakkuma.ui.components.ExpressiveSnackbar 37 + import com.derakkuma.ui.components.ExpressiveScaffold 34 38 import com.derakkuma.ui.components.SplitButtonRow 35 39 import com.derakkuma.ui.theme.* 36 40 import io.github.vinceglb.confettikit.compose.ConfettiKit ··· 64 68 } 65 69 } 66 70 67 - Scaffold( 71 + ExpressiveScaffold( 68 72 contentWindowInsets = WindowInsets(0.dp), 69 73 ) { padding -> 70 74 Column( ··· 72 76 verticalArrangement = Arrangement.spacedBy(12.dp), 73 77 ) { 74 78 Column(Modifier.padding(horizontal = 12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { 75 - Text("App Icon", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText) 79 + Text("App Icon", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 76 80 } 77 81 78 82 SplitButtonRow( ··· 84 88 selected = previewMode, 85 89 onSelect = { previewMode = it }, 86 90 modifier = Modifier.padding(horizontal = 12.dp), 87 - minOptionWidth = 96.dp, 88 - unselectedContainerColor = CardWhite, 89 91 ) 90 92 91 93 LazyVerticalGrid( ··· 114 116 } 115 117 116 118 pendingPartnerIcon?.let { option -> 117 - AlertDialog( 119 + ExpressiveAlertDialog( 118 120 onDismissRequest = { pendingPartnerIcon = null }, 119 121 title = { Text("Support Derakkuma") }, 120 122 text = { ··· 124 126 } 125 127 }, 126 128 confirmButton = { 127 - TextButton( 129 + ExpressiveTextButton( 128 130 onClick = { 129 131 scope.launch { 130 132 val nonce = DonationUnlock.friendCodeNonce(cacheStore) ··· 137 139 }, 138 140 dismissButton = { 139 141 Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { 140 - TextButton(onClick = { pendingPartnerIcon = null }) { 142 + ExpressiveTextButton(onClick = { pendingPartnerIcon = null }) { 141 143 Text("Not now") 142 144 } 143 - TextButton( 145 + ExpressiveTextButton( 144 146 onClick = { 145 147 scope.launch { 146 148 checkingRestore = true ··· 167 169 168 170 if (snackMsg != null) { 169 171 Popup(alignment = Alignment.BottomCenter) { 170 - Snackbar( 172 + ExpressiveSnackbar( 171 173 modifier = Modifier.padding(12.dp), 172 - shape = RoundedCornerShape(8.dp), 173 - action = { TextButton(onClick = { snackMsg = null }) { Text("OK") } }, 174 + action = { ExpressiveTextButton(onClick = { snackMsg = null }) { Text("OK") } }, 174 175 ) { Text(snackMsg!!) } 175 176 } 176 177 } ··· 200 201 201 202 @Composable 202 203 private fun AppIconCard(option: AppIconOption, selected: Boolean, alternatePreview: Boolean, onClick: () -> Unit) { 203 - Card( 204 + ExpressiveGlassCard( 204 205 modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), 205 - shape = RoundedCornerShape(26.dp), 206 - colors = CardDefaults.cardColors(containerColor = if (selected) Color(0xFFFFCDDD) else CardWhite), 206 + cornerRadius = 26.dp, 207 + containerColor = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHigh, 207 208 ) { 208 209 Column( 209 210 modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 8.dp), ··· 211 212 verticalArrangement = Arrangement.spacedBy(6.dp), 212 213 ) { 213 214 AppIconPreview(option = option, alternatePreview = alternatePreview) 214 - Text(option.label, fontSize = 11.sp, fontWeight = FontWeight.SemiBold, color = DarkText, maxLines = 1) 215 + Text( 216 + option.label, 217 + fontSize = 11.sp, 218 + fontWeight = FontWeight.SemiBold, 219 + color = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface, 220 + maxLines = 1, 221 + ) 215 222 } 216 223 } 217 224 } ··· 235 242 value = if (usesIosPreview) null else loadAppIconPreviewAsset(fgName) 236 243 } 237 244 val (androidMonochromeBackground, androidMonochromeForeground) = platformMonochromeAppIconColors() 245 + val defaultPreviewBackground = MaterialTheme.colorScheme.primaryContainer 238 246 239 - val previewModifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(18.dp)).let { modifier -> 240 - if (usesIosPreview) modifier else modifier.background(if (alternatePreview) androidMonochromeBackground else Color(0xFFEAF7FF)) 247 + val previewModifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(MaterialTheme.shapes.extraLarge).let { modifier -> 248 + if (usesIosPreview) modifier else modifier.background(if (alternatePreview) androidMonochromeBackground else defaultPreviewBackground) 241 249 } 242 250 243 251 Box(
+40 -51
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/AppSettingsTab.kt
··· 2 2 3 3 import androidx.compose.foundation.layout.* 4 4 import androidx.compose.foundation.rememberScrollState 5 - import androidx.compose.foundation.shape.RoundedCornerShape 6 5 import androidx.compose.foundation.verticalScroll 7 6 import androidx.compose.material.icons.Icons 8 7 import androidx.compose.material.icons.automirrored.filled.Logout ··· 15 14 import androidx.compose.ui.unit.dp 16 15 import androidx.compose.ui.unit.sp 17 16 import com.derakkuma.data.CacheStore 17 + import com.derakkuma.ui.components.ExpressiveButton 18 + import com.derakkuma.ui.components.ExpressiveGlassCard 19 + import com.derakkuma.ui.components.ExpressiveSwitch 20 + import com.derakkuma.ui.components.ExpressiveOutlinedButton 21 + import com.derakkuma.ui.components.ExpressiveTextButton 22 + import com.derakkuma.ui.components.ExpressiveAlertDialog 23 + import com.derakkuma.ui.components.ExpressiveSnackbar 24 + import com.derakkuma.ui.components.ExpressiveScaffold 18 25 import com.derakkuma.ui.components.SplitButtonRow 19 26 import com.derakkuma.ui.theme.* 20 27 import io.github.samuolis.posthog.PostHog ··· 31 38 var snackMsg by remember { mutableStateOf<String?>(null) } 32 39 var showLogoutConfirm by remember { mutableStateOf(false) } 33 40 34 - Scaffold( 41 + ExpressiveScaffold( 35 42 contentWindowInsets = WindowInsets(0.dp), 36 43 snackbarHost = { 37 44 if (snackMsg != null) { 38 - Snackbar( 45 + ExpressiveSnackbar( 39 46 modifier = Modifier.padding(12.dp), 40 - shape = RoundedCornerShape(8.dp), 41 - action = { TextButton(onClick = { snackMsg = null }) { Text("OK") } }, 47 + action = { ExpressiveTextButton(onClick = { snackMsg = null }) { Text("OK") } }, 42 48 ) { Text(snackMsg!!) } 43 49 } 44 50 }, ··· 47 53 modifier = Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).padding(start = 12.dp, end = 12.dp, bottom = 88.dp), 48 54 verticalArrangement = Arrangement.spacedBy(8.dp), 49 55 ) { 50 - Text("App Settings", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(bottom = 4.dp)) 56 + Text("App Settings", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 4.dp)) 51 57 // Account section 52 - Card( 58 + ExpressiveGlassCard( 53 59 modifier = Modifier.fillMaxWidth(), 54 - shape = RoundedCornerShape(8.dp), 55 - colors = CardDefaults.cardColors(containerColor = CardWhite), 56 60 ) { 57 61 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { 58 - Text("Account", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 59 - Text("Sign out of your SEGA account. You'll need to log in again to access your data.", fontSize = 12.sp, color = MutedText) 60 - OutlinedButton( 62 + Text("Account", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 63 + Text("Sign out of your SEGA account. You'll need to log in again to access your data.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 64 + ExpressiveOutlinedButton( 61 65 onClick = { showLogoutConfirm = true }, 62 66 modifier = Modifier.fillMaxWidth(), 63 - shape = RoundedCornerShape(8.dp), 64 - colors = ButtonDefaults.outlinedButtonColors(contentColor = DangerRed), 65 67 ) { 66 68 Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = null, modifier = Modifier.size(18.dp)) 67 69 Spacer(Modifier.width(8.dp)) ··· 71 73 } 72 74 73 75 // Appearance section 74 - Card( 76 + ExpressiveGlassCard( 75 77 modifier = Modifier.fillMaxWidth(), 76 - shape = RoundedCornerShape(8.dp), 77 - colors = CardDefaults.cardColors(containerColor = CardWhite), 78 78 ) { 79 79 Column(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { 80 80 Column { 81 - Text("Appearance", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 82 - Text("Customize Derakkuma's look and feel", fontSize = 12.sp, color = MutedText) 81 + Text("Appearance", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 82 + Text("Customize Derakkuma's look and feel", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 83 83 } 84 84 85 85 var selectedIconStyle by remember(iconStyle) { mutableStateOf(iconStyle.takeIf { it == "flat" } ?: "fun") } ··· 101 101 verticalAlignment = Alignment.CenterVertically, 102 102 ) { 103 103 Column(modifier = Modifier.weight(1f)) { 104 - Text("Park animation", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 105 - Text("Scrolling park animation above the navbar", fontSize = 11.sp, color = MutedText) 104 + Text("Park animation", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 105 + Text("Scrolling park animation above the navbar", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 106 106 } 107 107 var parkState by remember { mutableStateOf(cacheStore.getSetting("parkAnimation") != "false") } 108 - Switch( 108 + ExpressiveSwitch( 109 109 checked = parkState, 110 110 onCheckedChange = { 111 111 parkState = it 112 112 cacheStore.putSetting("parkAnimation", if (it) "true" else "false") 113 - }, 114 - colors = SwitchDefaults.colors(checkedTrackColor = PrimaryBlue), 113 + } 115 114 ) 116 115 } 117 116 } ··· 119 118 120 119 // Analytics section 121 120 if (Secrets.POSTHOG_API_KEY.isNotEmpty()) { 122 - Card( 121 + ExpressiveGlassCard( 123 122 modifier = Modifier.fillMaxWidth(), 124 - shape = RoundedCornerShape(8.dp), 125 - colors = CardDefaults.cardColors(containerColor = CardWhite), 126 123 ) { 127 124 Row( 128 125 modifier = Modifier.fillMaxWidth().padding(16.dp), ··· 130 127 verticalAlignment = Alignment.CenterVertically, 131 128 ) { 132 129 Column(modifier = Modifier.weight(1f)) { 133 - Text("Analytics", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 134 - Text("Fully anonymous and private", fontSize = 12.sp, color = MutedText) 130 + Text("Analytics", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 131 + Text("Fully anonymous and private", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 135 132 } 136 133 var analyticsEnabled by remember { mutableStateOf(cacheStore.getSetting("analyticsOptOut") != "true") } 137 - Switch( 134 + ExpressiveSwitch( 138 135 checked = analyticsEnabled, 139 136 onCheckedChange = { 140 137 analyticsEnabled = it 141 138 cacheStore.putSetting("analyticsOptOut", if (it) "false" else "true") 142 139 if (it) PostHog.optIn() else PostHog.optOut() 143 - }, 144 - colors = SwitchDefaults.colors(checkedTrackColor = PrimaryBlue), 140 + } 145 141 ) 146 142 } 147 143 } 148 144 } 149 145 150 146 // Cache section 151 - Card( 147 + ExpressiveGlassCard( 152 148 modifier = Modifier.fillMaxWidth(), 153 - shape = RoundedCornerShape(8.dp), 154 - colors = CardDefaults.cardColors(containerColor = CardWhite), 155 149 ) { 156 150 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { 157 - Text("Cache", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 158 - Text("Clear cached profile, play log, rating data, and app preferences. Useful if data appears wrong or corrupted.", fontSize = 12.sp, color = MutedText) 159 - Button( 151 + Text("Cache", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 152 + Text("Clear cached profile, play log, rating data, and app preferences. Useful if data appears wrong or corrupted.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 153 + ExpressiveButton( 160 154 onClick = { 161 155 cacheStore.clear() 162 156 snackMsg = "Cache cleared" 163 157 onCacheCleared() 164 158 }, 165 159 modifier = Modifier.fillMaxWidth(), 166 - shape = RoundedCornerShape(8.dp), 167 - colors = ButtonDefaults.buttonColors(containerColor = DangerRed), 160 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), 168 161 ) { 169 162 Icon(Icons.Default.DeleteSweep, contentDescription = null, modifier = Modifier.size(18.dp)) 170 163 Spacer(Modifier.width(8.dp)) ··· 174 167 } 175 168 176 169 // About section 177 - Card( 170 + ExpressiveGlassCard( 178 171 modifier = Modifier.fillMaxWidth(), 179 - shape = RoundedCornerShape(8.dp), 180 - colors = CardDefaults.cardColors(containerColor = CardWhite), 181 172 ) { 182 173 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 183 - Text("About", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 174 + Text("About", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 184 175 // Row( 185 176 // modifier = Modifier.fillMaxWidth(), 186 177 // horizontalArrangement = Arrangement.SpaceBetween, ··· 195 186 // Text("Built with", fontSize = 13.sp, color = MutedText) 196 187 // Text("Compose Multiplatform", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 197 188 // } 198 - Text("Derakkuma is an unofficial client for maimai DX NET. Not affiliated with SEGA.", fontSize = 11.sp, color = MutedText) 189 + Text("Derakkuma is an unofficial client for maimai DX NET. Not affiliated with SEGA.", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 199 190 } 200 191 } 201 192 } ··· 203 194 204 195 // Logout confirmation dialog 205 196 if (showLogoutConfirm) { 206 - AlertDialog( 197 + ExpressiveAlertDialog( 207 198 onDismissRequest = { showLogoutConfirm = false }, 208 199 title = { Text("Log Out?") }, 209 200 text = { Text("You'll need to log in again to access your data.") }, 210 201 confirmButton = { 211 - TextButton( 202 + ExpressiveTextButton( 212 203 onClick = { 213 204 showLogoutConfirm = false 214 205 onLogout() 215 206 }, 216 - colors = ButtonDefaults.textButtonColors(contentColor = DangerRed), 217 - ) { Text("Log Out", fontWeight = FontWeight.Bold) } 207 + ) { Text("Log Out", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.error) } 218 208 }, 219 209 dismissButton = { 220 - TextButton(onClick = { showLogoutConfirm = false }) { Text("Cancel") } 210 + ExpressiveTextButton(onClick = { showLogoutConfirm = false }) { Text("Cancel") } 221 211 }, 222 - containerColor = CardWhite, 223 212 ) 224 213 } 225 214 }
+4 -4
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/AtmosphereTab.kt
··· 5 5 import androidx.compose.foundation.layout.padding 6 6 import androidx.compose.foundation.rememberScrollState 7 7 import androidx.compose.foundation.verticalScroll 8 + import androidx.compose.material3.MaterialTheme 8 9 import androidx.compose.material3.Text 9 - import androidx.compose.material3.pulltorefresh.PullToRefreshBox 10 10 import androidx.compose.runtime.* 11 11 import androidx.compose.ui.Modifier 12 12 import androidx.compose.ui.text.font.FontWeight ··· 17 17 import com.derakkuma.data.CacheStore 18 18 import com.derakkuma.data.MaimaiClient 19 19 import com.derakkuma.data.PlayStore 20 - import com.derakkuma.ui.theme.DarkText 20 + import com.derakkuma.ui.components.ExpressivePullToRefreshBox 21 21 22 22 @Composable 23 23 fun AtmosphereTab( ··· 31 31 var refreshKey by remember { mutableStateOf(0) } 32 32 var refreshing by remember { mutableStateOf(false) } 33 33 34 - PullToRefreshBox( 34 + ExpressivePullToRefreshBox( 35 35 isRefreshing = refreshing, 36 36 onRefresh = { 37 37 refreshing = true ··· 43 43 .verticalScroll(rememberScrollState()) 44 44 .padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 88.dp), 45 45 ) { 46 - Text("Atmosphere", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(bottom = 8.dp)) 46 + Text("Atmosphere", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 8.dp)) 47 47 AtProtoSection( 48 48 client = atProtoClient, 49 49 cacheStore = cacheStore,
+24 -21
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/BubbleTab.kt
··· 4 4 import androidx.compose.foundation.lazy.LazyColumn 5 5 import androidx.compose.foundation.lazy.items 6 6 import androidx.compose.foundation.shape.CircleShape 7 - import androidx.compose.foundation.shape.RoundedCornerShape 7 + import androidx.compose.material.icons.automirrored.outlined.BluetoothSearching 8 8 import androidx.compose.material.icons.Icons 9 - import androidx.compose.material.icons.outlined.BluetoothSearching 10 9 import androidx.compose.material.icons.outlined.PersonAdd 11 10 import androidx.compose.material3.* 12 11 import androidx.compose.runtime.* ··· 21 20 import com.derakkuma.data.CacheStore 22 21 import com.derakkuma.data.MaimaiClient 23 22 import com.derakkuma.data.Profile 23 + import com.derakkuma.ui.components.ExpressiveButton 24 + import com.derakkuma.ui.components.ExpressiveGlassCard 25 + import com.derakkuma.ui.components.ExpressiveIconSurface 26 + import com.derakkuma.ui.components.ExpressiveLoadingIndicator 24 27 import com.derakkuma.ui.images.MaimaiRemoteImage 25 28 import com.derakkuma.ui.theme.* 26 29 import kotlinx.coroutines.launch ··· 42 45 DisposableEffect(Unit) { onDispose { manager.stop() } } 43 46 44 47 Column(Modifier.fillMaxSize().padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { 45 - Text("Bubble", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText) 46 - Card(shape = RoundedCornerShape(14.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 48 + Text("Bubble", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 49 + ExpressiveGlassCard(cornerRadius = 28.dp) { 47 50 Column(Modifier.fillMaxWidth().padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { 48 51 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { 49 - Surface(shape = CircleShape, color = PrimaryBlue.copy(alpha = 0.12f), modifier = Modifier.size(42.dp)) { 50 - Box(contentAlignment = Alignment.Center) { Icon(Icons.Outlined.BluetoothSearching, null, tint = PrimaryBlue) } 52 + ExpressiveIconSurface(size = 42.dp, containerColor = MaterialTheme.colorScheme.primaryContainer) { 53 + Box(contentAlignment = Alignment.Center) { Icon(Icons.AutoMirrored.Outlined.BluetoothSearching, null, tint = MaterialTheme.colorScheme.primary) } 51 54 } 52 55 Column(Modifier.weight(1f)) { 53 - Text("Nearby friend-code exchange", fontWeight = FontWeight.Bold, color = DarkText) 54 - Text("Broadcast your maimai friend code and player name over BLE to discover nearby Maimai players on Derakkuma.", fontSize = 12.sp, color = MutedText, lineHeight = 16.sp) 56 + Text("Nearby friend-code exchange", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 57 + Text("Broadcast your maimai friend code and player name over BLE to discover nearby Maimai players on Derakkuma.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, lineHeight = 16.sp) 55 58 } 56 59 } 57 - if (!manager.isSupported()) Text("BLE advertising/scanning is not available on this device.", color = DangerRed, fontSize = 12.sp) 58 - if (!canBubble) Text("Open Home/Friends once first so Derakkuma can cache your player name and friend code.", color = MutedText, fontSize = 12.sp) 59 - message?.let { Text(it, color = MutedText, fontSize = 12.sp) } 60 - Button( 60 + if (!manager.isSupported()) Text("BLE advertising/scanning is not available on this device.", color = MaterialTheme.colorScheme.error, fontSize = 12.sp) 61 + if (!canBubble) Text("Open Home/Friends once first so Derakkuma can cache your player name and friend code.", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 62 + message?.let { Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) } 63 + ExpressiveButton( 61 64 enabled = manager.isSupported() && canBubble, 62 65 onClick = { 63 66 scope.launch { ··· 70 73 } 71 74 } 72 75 }, 73 - colors = ButtonDefaults.buttonColors(containerColor = if (broadcasting) DangerRed else PrimaryBlue), 76 + colors = ButtonDefaults.buttonColors(containerColor = if (broadcasting) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary), 74 77 modifier = Modifier.fillMaxWidth(), 75 78 ) { Text(if (broadcasting) "Stop Bubble" else "Start Bubble") } 76 79 } 77 80 } 78 81 79 - Text("Nearby", fontWeight = FontWeight.Bold, color = DarkText) 82 + Text("Nearby", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 80 83 if (peers.isEmpty()) { 81 - Text(if (broadcasting) "Looking for nearby players..." else "Start Bubble to discover nearby players.", color = MutedText, fontSize = 13.sp) 84 + Text(if (broadcasting) "Looking for nearby players..." else "Start Bubble to discover nearby players.", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp) 82 85 } 83 86 LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(bottom = 96.dp)) { 84 87 items(peers, key = { it.id }) { peer -> ··· 120 123 121 124 @Composable 122 125 private fun BubblePeerRow(peer: BubblePeer, busy: Boolean, onRequest: () -> Unit) { 123 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(12.dp)) { 126 + ExpressiveGlassCard(cornerRadius = 24.dp) { 124 127 Row(Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { 125 128 MaimaiRemoteImage(peer.iconUrl, modifier = Modifier.size(42.dp), contentDescription = null) 126 129 Column(Modifier.weight(1f)) { 127 - Text(peer.playerName.ifBlank { "Nearby player" }, fontWeight = FontWeight.Bold, color = DarkText) 128 - Text("${peer.friendCode} · ${signalLabel(peer.rssi)}", fontSize = 12.sp, color = MutedText) 129 - if (peer.comment.isNotBlank()) Text(peer.comment, fontSize = 11.sp, color = MutedText, maxLines = 2) 130 + Text(peer.playerName.ifBlank { "Nearby player" }, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 131 + Text("${peer.friendCode} · ${signalLabel(peer.rssi)}", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 132 + if (peer.comment.isNotBlank()) Text(peer.comment, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2) 130 133 } 131 - Button(onClick = onRequest, enabled = !busy, colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)) { 132 - if (busy) CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp, color = CardWhite) else Icon(Icons.Outlined.PersonAdd, null, modifier = Modifier.size(18.dp)) 134 + ExpressiveButton(onClick = onRequest, enabled = !busy, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)) { 135 + if (busy) ExpressiveLoadingIndicator(modifier = Modifier, color = MaterialTheme.colorScheme.onPrimary, size = 16.dp) else Icon(Icons.Outlined.PersonAdd, null, modifier = Modifier.size(18.dp)) 133 136 Spacer(Modifier.width(6.dp)) 134 137 Text("Request") 135 138 }
+68 -81
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/CollectionTab.kt
··· 1 1 package com.derakkuma.ui.tabs 2 2 3 + import com.derakkuma.ui.components.ExpressiveLoadingIndicator 3 4 import androidx.compose.foundation.layout.* 4 5 import androidx.compose.foundation.lazy.LazyColumn 5 6 import androidx.compose.foundation.lazy.LazyRow 6 7 import androidx.compose.foundation.lazy.items 7 - import androidx.compose.foundation.shape.RoundedCornerShape 8 8 import androidx.compose.material.icons.Icons 9 9 import androidx.compose.material.icons.outlined.* 10 10 import androidx.compose.material3.* ··· 31 31 import com.derakkuma.data.TourMemberFormationPage 32 32 import com.derakkuma.data.TourMemberSelectionPage 33 33 import com.derakkuma.ui.components.EmptyState 34 + import com.derakkuma.ui.components.ExpressiveButton 35 + import com.derakkuma.ui.components.ExpressiveGlassCard 36 + import com.derakkuma.ui.components.ExpressiveFilterChip 37 + import com.derakkuma.ui.components.ExpressiveTextButton 34 38 import com.derakkuma.ui.components.LoadingState 35 39 import com.derakkuma.ui.images.BundledIcon 36 40 import com.derakkuma.ui.images.BundledImage 37 41 import com.derakkuma.ui.images.MaimaiRemoteImage 38 - import com.derakkuma.ui.theme.CardWhite 39 - import com.derakkuma.ui.theme.DarkText 40 - import com.derakkuma.ui.theme.MutedText 41 - import com.derakkuma.ui.theme.PrimaryBlue 42 42 import kotlinx.coroutines.launch 43 43 44 44 @Composable ··· 92 92 } 93 93 94 94 Column(Modifier.fillMaxSize().padding(start = 12.dp, end = 12.dp, top = 12.dp)) { 95 - Text("Collection", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(bottom = 8.dp)) 95 + Text("Collection", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 8.dp)) 96 96 LazyRow(horizontalArrangement = Arrangement.spacedBy(2.dp), contentPadding = PaddingValues(0.dp), modifier = Modifier.fillMaxWidth()) { 97 97 items(CollectionKind.entries) { tab -> 98 - TextButton( 98 + ExpressiveTextButton( 99 99 onClick = { 100 100 kind = tab 101 101 genre = "0" ··· 109 109 contentPadding = PaddingValues(horizontal = 6.dp, vertical = 4.dp), 110 110 modifier = Modifier.height(32.dp), 111 111 ) { 112 - Text(tab.label, fontSize = 11.sp, fontWeight = if (kind == tab) FontWeight.Bold else FontWeight.Normal, color = Color.White, maxLines = 1) 112 + Text(tab.label, fontSize = 11.sp, fontWeight = if (kind == tab) FontWeight.Bold else FontWeight.Normal, color = MaterialTheme.colorScheme.onPrimary, maxLines = 1) 113 113 } 114 114 } 115 115 } ··· 118 118 return@Column 119 119 } 120 120 if (loading) { 121 - LoadingState(Modifier.fillMaxSize(), color = PrimaryBlue) 121 + LoadingState(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) 122 122 return@Column 123 123 } 124 124 page?.let { data -> ··· 155 155 } 156 156 } 157 157 if (kind != CollectionKind.TITLE && kind != CollectionKind.TOUR_MEMBER && data.genres.isNotEmpty()) item { GenreSelector(data.genres, genre) { genre = it } } 158 - message?.let { item { Text(it, color = MutedText, fontSize = 12.sp) } } 158 + message?.let { item { Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) } } 159 159 if (kind == CollectionKind.TOUR_MEMBER && showFormation) { 160 160 arrangement?.let { edited -> 161 161 item { ··· 282 282 return@LazyColumn 283 283 } 284 284 visibleSections.forEach { section -> 285 - item { Text(section.title, color = DarkText, fontWeight = FontWeight.Bold, fontSize = 15.sp, modifier = Modifier.padding(top = 8.dp)) } 285 + item { Text(section.title, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 15.sp, modifier = Modifier.padding(top = 8.dp)) } 286 286 items(section.items.size, key = { index -> "${kind.name}:${section.id}:$index:${section.items[index].name}" }) { index -> 287 287 val item = section.items[index] 288 288 CollectionItemCard( ··· 310 310 } 311 311 if (!loading && visibleSections.isEmpty()) { 312 312 item { 313 - Text("No ${kind.label.lowercase()} entries found.", color = MutedText, modifier = Modifier.padding(12.dp)) 313 + Text("No ${kind.label.lowercase()} entries found.", color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(12.dp)) 314 314 } 315 315 } 316 316 } 317 317 } ?: Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 318 - if (loading) CircularProgressIndicator() else Text(message ?: "No collection data", color = MutedText) 318 + if (loading) ExpressiveLoadingIndicator() else Text(message ?: "No collection data", color = MaterialTheme.colorScheme.onSurfaceVariant) 319 319 } 320 320 } 321 321 } 322 322 323 323 @Composable 324 324 private fun CurrentCollectionCard(item: CollectionItem?, page: CollectionPage) { 325 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(12.dp)) { 325 + ExpressiveGlassCard(cornerRadius = 24.dp) { 326 326 Row(Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { 327 327 if (page.kind == CollectionKind.NAMEPLATE || page.kind == CollectionKind.FRAME) { 328 328 MaimaiRemoteImage(item?.imageUrl, modifier = Modifier.width(116.dp).height(42.dp), contentDescription = null, contentScale = ContentScale.Fit) ··· 330 330 MaimaiRemoteImage(item?.imageUrl, modifier = Modifier.size(58.dp), contentDescription = null) 331 331 } 332 332 Column(Modifier.weight(1f)) { 333 - Text("Currently using", color = MutedText, fontSize = 11.sp) 334 - Text(item?.name ?: "Unknown", color = DarkText, fontWeight = FontWeight.Bold) 335 - item?.category?.takeIf { it.isNotBlank() }?.let { Text(it, color = MutedText, fontSize = 12.sp) } 336 - Text("Favorite ${page.favoriteCount}/${page.favoriteTotal} · Total ${page.itemCount}", color = MutedText, fontSize = 12.sp) 333 + Text("Currently using", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 334 + Text(item?.name ?: "Unknown", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold) 335 + item?.category?.takeIf { it.isNotBlank() }?.let { Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) } 336 + Text("Favorite ${page.favoriteCount}/${page.favoriteTotal} · Total ${page.itemCount}", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 337 337 } 338 338 } 339 339 } ··· 342 342 @Composable 343 343 private fun GenreSelector(genres: List<CollectionGenre>, selected: String, onSelect: (String) -> Unit) { 344 344 LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) { 345 - items(genres) { g -> CollectionFilterChip(selected = selected == g.value, onClick = { onSelect(g.value) }) { Text(g.label, fontSize = 11.sp) } } 345 + items(genres) { g -> CollectionExpressiveFilterChip(selected = selected == g.value, onClick = { onSelect(g.value) }) { Text(g.label, fontSize = 11.sp) } } 346 346 } 347 347 } 348 348 ··· 350 350 private fun RareSelector(selected: String, onSelect: (String) -> Unit) { 351 351 Row(horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) { 352 352 listOf("0" to "Normal", "1" to "Bronze", "2" to "Silver", "3" to "Gold", "4" to "Rainbow").forEach { (value, label) -> 353 - CollectionFilterChip(selected = selected == value, onClick = { onSelect(value) }) { Text(label, fontSize = 11.sp) } 353 + CollectionExpressiveFilterChip(selected = selected == value, onClick = { onSelect(value) }) { Text(label, fontSize = 11.sp) } 354 354 } 355 355 } 356 356 } 357 357 358 358 @Composable 359 - private fun CollectionFilterChip(selected: Boolean, onClick: () -> Unit, label: @Composable () -> Unit) { 360 - FilterChip( 359 + private fun CollectionExpressiveFilterChip(selected: Boolean, onClick: () -> Unit, label: @Composable () -> Unit) { 360 + ExpressiveFilterChip( 361 361 selected = selected, 362 362 onClick = onClick, 363 363 label = label, 364 - colors = FilterChipDefaults.filterChipColors( 365 - containerColor = Color.White, 366 - labelColor = DarkText, 367 - selectedContainerColor = PrimaryBlue, 368 - selectedLabelColor = Color.White, 369 - ), 370 - border = FilterChipDefaults.filterChipBorder( 371 - enabled = true, 372 - selected = selected, 373 - borderColor = Color.Transparent, 374 - selectedBorderColor = PrimaryBlue, 375 - ), 376 364 ) 377 365 } 378 366 ··· 380 368 private fun TourMemberControls(page: CollectionPage, selected: String, showingFormation: Boolean, onSelect: (String) -> Unit, onFormation: () -> Unit) { 381 369 Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { 382 370 if (page.joinedMembers.isNotBlank() || page.starCounts.isNotEmpty()) { 383 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(10.dp)) { 371 + ExpressiveGlassCard(cornerRadius = 22.dp) { 384 372 Column(Modifier.fillMaxWidth().padding(10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { 385 - if (page.joinedMembers.isNotBlank()) Text("${page.joinedMembers.filter { it.isDigit() }} partners", color = DarkText, fontWeight = FontWeight.Bold, fontSize = 13.sp) 373 + if (page.joinedMembers.isNotBlank()) Text("${page.joinedMembers.filter { it.isDigit() }} partners", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 13.sp) 386 374 if (page.starCounts.isNotEmpty()) { 387 375 Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 388 376 page.starCounts.forEach { (stars, count) -> 389 377 Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { 390 - Text("$stars", color = MutedText, fontSize = 11.sp) 378 + Text("$stars", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 391 379 BundledIcon(BundledImage.IconStar, modifier = Modifier.size(12.dp), contentDescription = "star") 392 - Text("×$count", color = MutedText, fontSize = 11.sp) 380 + Text("×$count", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 393 381 } 394 382 } 395 383 } ··· 398 386 } 399 387 } 400 388 Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { 401 - CollectionFilterChip(selected = selected != "event", onClick = { onSelect("0") }) { Text("Area") } 402 - CollectionFilterChip(selected = selected == "event", onClick = { onSelect("event") }) { Text("Event Area") } 389 + CollectionExpressiveFilterChip(selected = selected != "event", onClick = { onSelect("0") }) { Text("Area") } 390 + CollectionExpressiveFilterChip(selected = selected == "event", onClick = { onSelect("event") }) { Text("Event Area") } 403 391 } 404 - Button( 392 + ExpressiveButton( 405 393 onClick = onFormation, 406 - shape = RoundedCornerShape(999.dp), 407 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue, contentColor = Color.White), 394 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary), 408 395 contentPadding = PaddingValues(horizontal = 14.dp, vertical = 8.dp), 409 396 ) { 410 397 Icon(Icons.Outlined.Groups, null, modifier = Modifier.size(18.dp)) ··· 438 425 SelectionEditor(selection = sel, onSelectCandidate = onSelectCandidate) 439 426 return@Column 440 427 } 441 - Text("Tour Members Formation", color = DarkText, fontWeight = FontWeight.Bold, fontSize = 15.sp) 428 + Text("Tour Members Formation", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 15.sp) 442 429 page.slots.groupBy { it.preset }.forEach { (preset, slots) -> 443 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(10.dp)) { 430 + ExpressiveGlassCard(cornerRadius = 24.dp) { 444 431 Column(Modifier.fillMaxWidth().padding(10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 445 432 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { 446 - Text(if (preset == 0) "Current Formation" else "Preset $preset", color = DarkText, fontWeight = FontWeight.Bold) 433 + Text(if (preset == 0) "Current Formation" else "Preset $preset", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold) 447 434 if (preset > 0) { 448 435 Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { 449 - TextButton(onClick = { onArrangePreset(preset) }, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)) { Text("Arrange", fontSize = 11.sp) } 450 - Button(onClick = { onSetPreset(preset) }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Use", fontSize = 11.sp) } 436 + ExpressiveTextButton(onClick = { onArrangePreset(preset) }, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)) { Text("Arrange", fontSize = 11.sp) } 437 + ExpressiveButton(onClick = { onSetPreset(preset) }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Use", fontSize = 11.sp) } 451 438 } 452 439 } 453 440 } ··· 456 443 Column(Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) { 457 444 MaimaiRemoteImage(slot.imageUrl, modifier = Modifier.size(48.dp), contentDescription = null) 458 445 TourMemberMeta(level = slot.level, awakening = slot.awakening, fontSize = 10) 459 - if (slot.locked) Text("Lock", color = MutedText, fontSize = 9.sp) 460 - TextButton(onClick = { onChangeSlot(preset, slot.position) }, contentPadding = PaddingValues(0.dp)) { Text("Change", fontSize = 10.sp) } 446 + if (slot.locked) Text("Lock", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 9.sp) 447 + ExpressiveTextButton(onClick = { onChangeSlot(preset, slot.position) }, contentPadding = PaddingValues(0.dp)) { Text("Change", fontSize = 10.sp) } 461 448 } 462 449 } 463 450 } ··· 469 456 470 457 @Composable 471 458 private fun SelectionEditor(selection: TourMemberSelectionPage, onSelectCandidate: (TourMemberCandidate) -> Unit) { 472 - Text("Choose member for Preset ${selection.preset} Slot ${selection.position}", color = DarkText, fontWeight = FontWeight.Bold, fontSize = 14.sp) 459 + Text("Choose member for Preset ${selection.preset} Slot ${selection.position}", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 14.sp) 473 460 selection.candidates.forEach { candidate -> 474 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(10.dp)) { 461 + ExpressiveGlassCard(cornerRadius = 22.dp) { 475 462 Row(Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { 476 463 MaimaiRemoteImage(candidate.imageUrl, modifier = Modifier.size(44.dp), contentDescription = null) 477 464 Column(Modifier.weight(1f)) { 478 - Text(candidate.name, color = DarkText, fontWeight = FontWeight.Bold, fontSize = 13.sp) 465 + Text(candidate.name, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 13.sp) 479 466 TourMemberMeta(level = candidate.level, awakening = candidate.awakening, fontSize = 11) 480 467 } 481 - Button(onClick = { onSelectCandidate(candidate) }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Set", fontSize = 11.sp) } 468 + ExpressiveButton(onClick = { onSelectCandidate(candidate) }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Set", fontSize = 11.sp) } 482 469 } 483 470 } 484 471 } ··· 486 473 487 474 @Composable 488 475 private fun ArrangementEditor(page: TourMemberArrangementPage, onChange: (TourMemberArrangementPage) -> Unit, onSave: () -> Unit) { 489 - Text("Arrange Preset ${page.preset}", color = DarkText, fontWeight = FontWeight.Bold, fontSize = 15.sp) 490 - Text("Use ↑/↓ to reorder members, then save.", color = MutedText, fontSize = 12.sp) 491 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(10.dp)) { 476 + Text("Arrange Preset ${page.preset}", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 15.sp) 477 + Text("Use ↑/↓ to reorder members, then save.", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 478 + ExpressiveGlassCard(cornerRadius = 24.dp) { 492 479 Column(Modifier.fillMaxWidth().padding(10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 493 480 page.slots.sortedBy { it.position }.forEach { slot -> 494 481 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { 495 482 MaimaiRemoteImage(slot.imageUrl, modifier = Modifier.size(42.dp), contentDescription = null) 496 483 Column(Modifier.weight(1f)) { 497 - Text("Slot ${slot.position}", color = DarkText, fontWeight = FontWeight.Bold, fontSize = 12.sp) 484 + Text("Slot ${slot.position}", color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 12.sp) 498 485 TourMemberMeta(level = slot.level, awakening = slot.awakening, fontSize = 10) 499 486 } 500 - TextButton(enabled = slot.position > 1, onClick = { onChange(page.copy(slots = page.slots.swapPositions(slot.position, slot.position - 1))) }) { Text("↑", fontSize = 16.sp) } 501 - TextButton(enabled = slot.position < 5, onClick = { onChange(page.copy(slots = page.slots.swapPositions(slot.position, slot.position + 1))) }) { Text("↓", fontSize = 16.sp) } 502 - FilterChip(selected = slot.lock == "1", onClick = { onChange(page.copy(slots = page.slots.map { if (it.position == slot.position) it.copy(lock = if (it.lock == "1") "0" else "1") else it })) }, label = { Text("Lock", fontSize = 10.sp) }) 487 + ExpressiveTextButton(enabled = slot.position > 1, onClick = { onChange(page.copy(slots = page.slots.swapPositions(slot.position, slot.position - 1))) }) { Text("↑", fontSize = 16.sp) } 488 + ExpressiveTextButton(enabled = slot.position < 5, onClick = { onChange(page.copy(slots = page.slots.swapPositions(slot.position, slot.position + 1))) }) { Text("↓", fontSize = 16.sp) } 489 + CollectionExpressiveFilterChip(selected = slot.lock == "1", onClick = { onChange(page.copy(slots = page.slots.map { if (it.position == slot.position) it.copy(lock = if (it.lock == "1") "0" else "1") else it })) }) { Text("Lock", fontSize = 10.sp) } 503 490 } 504 491 } 505 - Button(onClick = onSave, modifier = Modifier.align(Alignment.End)) { Text("Save arrangement") } 492 + ExpressiveButton(onClick = onSave, modifier = Modifier.align(Alignment.End)) { Text("Save arrangement") } 506 493 } 507 494 } 508 495 } ··· 510 497 @Composable 511 498 private fun TourMemberMeta(level: String, awakening: String, fontSize: Int) { 512 499 Row(horizontalArrangement = Arrangement.spacedBy(3.dp), verticalAlignment = Alignment.CenterVertically) { 513 - if (level.isNotBlank()) Text(level, color = MutedText, fontSize = fontSize.sp) 500 + if (level.isNotBlank()) Text(level, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = fontSize.sp) 514 501 if (awakening.isNotBlank()) { 515 502 BundledIcon(BundledImage.IconStar, modifier = Modifier.size((fontSize + 2).dp), contentDescription = "star") 516 - Text(awakening, color = MutedText, fontSize = fontSize.sp) 503 + Text(awakening, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = fontSize.sp) 517 504 } 518 505 } 519 506 } ··· 536 523 TitleCollectionItemCard(item = item, onSet = onSet, onFavorite = onFavorite) 537 524 return 538 525 } 539 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(10.dp)) { 526 + ExpressiveGlassCard(cornerRadius = 22.dp) { 540 527 Row(Modifier.fillMaxWidth().padding(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { 541 528 MaimaiRemoteImage(item.imageUrl, modifier = Modifier.size(if (kind == CollectionKind.FRAME) 72.dp else 54.dp), contentDescription = null) 542 529 Column(Modifier.weight(1f)) { 543 - Text(item.name, color = if (item.isLocked) MutedText else DarkText, fontWeight = FontWeight.Bold, fontSize = 14.sp) 530 + Text(item.name, color = if (item.isLocked) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 14.sp) 544 531 val meta = listOf(item.category, item.level).filter { it.isNotBlank() }.joinToString(" · ") 545 - if (meta.isNotBlank()) Text(meta, color = MutedText, fontSize = 11.sp) 532 + if (meta.isNotBlank()) Text(meta, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 546 533 if (item.awakening.isNotBlank()) TourMemberMeta(level = "", awakening = item.awakening, fontSize = 11) 547 - if (item.condition.isNotBlank()) Text(item.condition, color = MutedText, fontSize = 11.sp, maxLines = 2) 534 + if (item.condition.isNotBlank()) Text(item.condition, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp, maxLines = 2) 548 535 } 549 536 Column(verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.End) { 550 - if (item.favoriteAction.isNotBlank()) TextButton(onClick = onFavorite) { Text(if (item.favoriteAction.contains("Off")) "Unfav" else "Fav", fontSize = 11.sp) } 551 - if (item.setAction.isNotBlank()) Button(onClick = onSet, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Set", fontSize = 11.sp) } 537 + if (item.favoriteAction.isNotBlank()) ExpressiveTextButton(onClick = onFavorite) { Text(if (item.favoriteAction.contains("Off")) "Unfav" else "Fav", fontSize = 11.sp) } 538 + if (item.setAction.isNotBlank()) ExpressiveButton(onClick = onSet, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Set", fontSize = 11.sp) } 552 539 } 553 540 } 554 541 } ··· 556 543 557 544 @Composable 558 545 private fun WideCollectionItemCard(item: CollectionItem, onSet: () -> Unit, onFavorite: () -> Unit) { 559 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(10.dp)) { 546 + ExpressiveGlassCard(cornerRadius = 24.dp) { 560 547 Column(Modifier.fillMaxWidth().padding(10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 561 548 MaimaiRemoteImage(item.imageUrl, modifier = Modifier.fillMaxWidth().height(86.dp), contentDescription = null, contentScale = ContentScale.Fit) 562 549 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { 563 550 Column(Modifier.weight(1f)) { 564 - Text(item.name, color = if (item.isLocked) MutedText else DarkText, fontWeight = FontWeight.Bold, fontSize = 14.sp) 565 - if (item.category.isNotBlank()) Text(item.category, color = MutedText, fontSize = 11.sp) 566 - if (item.condition.isNotBlank()) Text(item.condition, color = MutedText, fontSize = 11.sp, maxLines = 2) 551 + Text(item.name, color = if (item.isLocked) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 14.sp) 552 + if (item.category.isNotBlank()) Text(item.category, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 553 + if (item.condition.isNotBlank()) Text(item.condition, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp, maxLines = 2) 567 554 } 568 555 Column(verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.End) { 569 - if (item.favoriteAction.isNotBlank()) TextButton(onClick = onFavorite) { Text(if (item.favoriteAction.contains("Off")) "Unfav" else "Fav", fontSize = 11.sp) } 570 - if (item.setAction.isNotBlank()) Button(onClick = onSet, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Set", fontSize = 11.sp) } 556 + if (item.favoriteAction.isNotBlank()) ExpressiveTextButton(onClick = onFavorite) { Text(if (item.favoriteAction.contains("Off")) "Unfav" else "Fav", fontSize = 11.sp) } 557 + if (item.setAction.isNotBlank()) ExpressiveButton(onClick = onSet, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Set", fontSize = 11.sp) } 571 558 } 572 559 } 573 560 } ··· 576 563 577 564 @Composable 578 565 private fun TitleCollectionItemCard(item: CollectionItem, onSet: () -> Unit, onFavorite: () -> Unit) { 579 - Card(colors = CardDefaults.cardColors(containerColor = CardWhite), shape = RoundedCornerShape(10.dp)) { 566 + ExpressiveGlassCard(cornerRadius = 18.dp) { 580 567 Row(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { 581 568 Column(Modifier.weight(1f)) { 582 - Text(item.name, color = if (item.isLocked) MutedText else DarkText, fontWeight = FontWeight.Bold, fontSize = 13.sp, maxLines = 2) 583 - if (item.category.isNotBlank()) Text(item.category, color = MutedText, fontSize = 10.sp, maxLines = 1) 569 + Text(item.name, color = if (item.isLocked) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold, fontSize = 13.sp, maxLines = 2) 570 + if (item.category.isNotBlank()) Text(item.category, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 10.sp, maxLines = 1) 584 571 } 585 572 Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { 586 - if (item.favoriteAction.isNotBlank()) TextButton(onClick = onFavorite) { Text(if (item.favoriteAction.contains("Off")) "Unfav" else "Fav", fontSize = 11.sp) } 587 - if (item.setAction.isNotBlank()) Button(onClick = onSet, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Set", fontSize = 11.sp) } 573 + if (item.favoriteAction.isNotBlank()) ExpressiveTextButton(onClick = onFavorite) { Text(if (item.favoriteAction.contains("Off")) "Unfav" else "Fav", fontSize = 11.sp) } 574 + if (item.setAction.isNotBlank()) ExpressiveButton(onClick = onSet, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) { Text("Set", fontSize = 11.sp) } 588 575 } 589 576 } 590 577 }
+58 -49
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/FavoritesTab.kt
··· 4 4 import androidx.compose.foundation.lazy.LazyColumn 5 5 import androidx.compose.foundation.lazy.items 6 6 import androidx.compose.foundation.rememberScrollState 7 - import androidx.compose.foundation.shape.RoundedCornerShape 8 7 import androidx.compose.foundation.verticalScroll 9 8 import androidx.compose.material.icons.Icons 10 9 import androidx.compose.material.icons.filled.Edit 11 10 import androidx.compose.material.icons.filled.Search 11 + import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 12 + import androidx.compose.material3.MaterialShapes 12 13 import androidx.compose.material3.* 14 + import androidx.compose.material3.toShape 13 15 import androidx.compose.runtime.* 14 16 import androidx.compose.ui.Alignment 15 17 import androidx.compose.ui.Modifier ··· 22 24 import com.derakkuma.atproto.syncFavoriteListToAtProto 23 25 import com.derakkuma.data.* 24 26 import com.derakkuma.ui.components.EmptyState 27 + import com.derakkuma.ui.components.ExpressiveGlassCard 28 + import com.derakkuma.ui.components.ExpressiveCheckbox 29 + import com.derakkuma.ui.components.ExpressiveLoadingIndicator 30 + import com.derakkuma.ui.components.ExpressiveScaffold 31 + import com.derakkuma.ui.components.ExpressiveSearchBar 32 + import com.derakkuma.ui.components.ExpressiveTextButton 33 + import com.derakkuma.ui.components.ExpressiveSnackbar 34 + import com.derakkuma.ui.components.ExpressiveFloatingActionButton 25 35 import com.derakkuma.ui.components.LoadingState 26 36 import com.derakkuma.ui.images.* 27 37 import com.derakkuma.ui.theme.* ··· 29 39 import kotlinx.coroutines.launch 30 40 import kotlinx.coroutines.withContext 31 41 42 + @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) 32 43 @Composable 33 44 fun FavoritesTab(client: MaimaiClient, cookie: String, maintenanceMode: Boolean = false, cacheStore: CacheStore = CacheStore(), atProtoClient: AtProtoClient? = null) { 34 45 var favorites by remember { mutableStateOf<FavoriteList?>(null) } ··· 60 71 loading = false 61 72 } 62 73 63 - Scaffold( 74 + ExpressiveScaffold( 64 75 contentWindowInsets = WindowInsets(0.dp), 65 76 snackbarHost = { 66 77 if (snackMsg != null) { 67 - Snackbar( 78 + ExpressiveSnackbar( 68 79 modifier = Modifier.padding(12.dp), 69 - shape = RoundedCornerShape(8.dp), 70 - action = { TextButton(onClick = { snackMsg = null }) { Text("OK") } }, 80 + action = { ExpressiveTextButton(onClick = { snackMsg = null }) { Text("OK") } }, 71 81 ) { Text(snackMsg!!) } 72 82 } 73 83 }, 74 84 floatingActionButton = { 75 85 if (favorites != null && !loading && !maintenanceMode) { 76 - FloatingActionButton( 86 + ExpressiveFloatingActionButton( 77 87 onClick = { 78 - if (loadingEdit) return@FloatingActionButton 88 + if (loadingEdit) return@ExpressiveFloatingActionButton 79 89 if (editing) { 80 90 // Save 81 91 val selectedIds = checkedSongs.filter { it.value }.keys.toList() ··· 118 128 } 119 129 } 120 130 }, 121 - containerColor = PrimaryBlue, 131 + containerColor = MaterialTheme.colorScheme.primary, 122 132 ) { 123 133 if (saving || loadingEdit) { 124 - CircularProgressIndicator(modifier = Modifier.size(24.dp), color = CardWhite, strokeWidth = 2.dp) 134 + ExpressiveLoadingIndicator(modifier = Modifier, color = MaterialTheme.colorScheme.onPrimary, size = 24.dp) 125 135 } else { 126 136 Icon( 127 137 if (editing) Icons.Default.Edit else Icons.Default.Edit, 128 138 contentDescription = if (editing) "Save" else "Edit", 129 - tint = CardWhite, 139 + tint = MaterialTheme.colorScheme.onPrimary, 130 140 ) 131 141 } 132 142 } ··· 140 150 Column(Modifier.fillMaxSize().padding(padding)) { 141 151 // Header + search 142 152 Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) { 143 - Text("Edit Favorites", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText) 153 + Text("Edit Favorites", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 144 154 Spacer(Modifier.height(4.dp)) 145 - Text("Select songs to favorite", fontSize = 12.sp, color = MutedText) 155 + Text("Select songs to favorite", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 146 156 Spacer(Modifier.height(8.dp)) 147 - OutlinedTextField( 148 - value = searchQuery, 149 - onValueChange = { searchQuery = it }, 157 + var searchActive by remember { mutableStateOf(false) } 158 + ExpressiveSearchBar( 159 + inputField = { 160 + SearchBarDefaults.InputField( 161 + query = searchQuery, 162 + onQueryChange = { searchQuery = it }, 163 + onSearch = { searchActive = false }, 164 + expanded = searchActive, 165 + onExpandedChange = { searchActive = it }, 166 + placeholder = { Text("Search songs...") }, 167 + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, 168 + ) 169 + }, 170 + expanded = searchActive, 171 + onExpandedChange = { searchActive = it }, 150 172 modifier = Modifier.fillMaxWidth(), 151 - placeholder = { Text("Search songs...") }, 152 - leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, 153 - singleLine = true, 154 - shape = RoundedCornerShape(8.dp), 155 - colors = OutlinedTextFieldDefaults.colors( 156 - unfocusedContainerColor = CardWhite, 157 - focusedContainerColor = CardWhite, 158 - ), 159 - ) 173 + ) {} 160 174 Spacer(Modifier.height(8.dp)) 161 175 } 162 176 ··· 170 184 } 171 185 } 172 186 173 - Card( 187 + ExpressiveGlassCard( 174 188 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).weight(1f), 175 - shape = RoundedCornerShape(8.dp), 176 - colors = CardDefaults.cardColors(containerColor = CardWhite), 189 + cornerRadius = 24.dp, 177 190 ) { 178 191 LazyColumn( 179 192 modifier = Modifier.fillMaxSize(), ··· 185 198 verticalAlignment = Alignment.CenterVertically, 186 199 horizontalArrangement = Arrangement.spacedBy(8.dp), 187 200 ) { 188 - Checkbox( 201 + ExpressiveCheckbox( 189 202 checked = checkedSongs[song.value] ?: false, 190 203 onCheckedChange = { checked -> 191 204 checkedSongs = checkedSongs.toMutableMap().apply { this[song.value] = checked } 192 205 }, 193 - colors = CheckboxDefaults.colors(checkedColor = PrimaryBlue), 194 206 ) 195 207 if (song.jacketUrl.isNotBlank()) { 196 208 MaimaiRemoteImage( 197 209 url = song.jacketUrl, 198 - modifier = Modifier.size(36.dp).clip(RoundedCornerShape(4.dp)), 210 + modifier = Modifier.size(36.dp).clip(MaterialShapes.Square.toShape()), 199 211 contentDescription = song.name, 200 212 contentScale = ContentScale.Crop, 201 213 ) 202 214 } 203 - Text(song.name, fontSize = 13.sp, color = DarkText, modifier = Modifier.weight(1f)) 215 + Text(song.name, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f)) 204 216 if (song.genre.isNotBlank()) { 205 - Text(song.genre, fontSize = 10.sp, color = MutedText) 217 + Text(song.genre, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 206 218 } 207 219 } 208 220 } ··· 210 222 if (filteredSongs.isEmpty()) { 211 223 item { 212 224 Box(Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) { 213 - Text("No songs match search", fontSize = 14.sp, color = MutedText) 225 + Text("No songs match search", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 214 226 } 215 227 } 216 228 } ··· 226 238 modifier = Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).padding(start = 12.dp, end = 12.dp, bottom = 88.dp), 227 239 ) { 228 240 // Header 229 - Card( 241 + ExpressiveGlassCard( 230 242 modifier = Modifier.fillMaxWidth(), 231 - shape = RoundedCornerShape(8.dp), 232 - colors = CardDefaults.cardColors(containerColor = CardWhite), 243 + cornerRadius = 24.dp, 233 244 ) { 234 245 Row( 235 246 modifier = Modifier.padding(16.dp).fillMaxWidth(), 236 247 horizontalArrangement = Arrangement.SpaceBetween, 237 248 verticalAlignment = Alignment.CenterVertically, 238 249 ) { 239 - Text("Favorites", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText) 240 - Text("${fav.current} / ${fav.max}", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue) 250 + Text("Favorites", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 251 + Text("${fav.current} / ${fav.max}", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) 241 252 } 242 253 } 243 254 ··· 247 258 val songsByGenre = fav.songs.groupBy { it.genre } 248 259 songsByGenre.forEach { (genre, songs) -> 249 260 if (genre.isNotBlank()) { 250 - Text(genre, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = PrimaryBlue, modifier = Modifier.padding(start = 4.dp, top = 8.dp)) 261 + Text(genre, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 4.dp, top = 8.dp)) 251 262 } 252 263 songs.forEach { song -> 253 - Card( 264 + ExpressiveGlassCard( 254 265 modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp), 255 - shape = RoundedCornerShape(6.dp), 256 - colors = CardDefaults.cardColors(containerColor = CardWhite), 266 + cornerRadius = 16.dp, 257 267 ) { 258 268 Row( 259 269 modifier = Modifier.padding(8.dp).fillMaxWidth(), ··· 263 273 if (song.jacketUrl.isNotBlank()) { 264 274 MaimaiRemoteImage( 265 275 url = song.jacketUrl, 266 - modifier = Modifier.size(44.dp).clip(RoundedCornerShape(4.dp)), 276 + modifier = Modifier.size(44.dp).clip(MaterialShapes.Square.toShape()), 267 277 contentDescription = song.name, 268 278 contentScale = ContentScale.Crop, 269 279 ) 270 280 } 271 281 Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { 272 - Text(song.name, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = DarkText, maxLines = 1) 282 + Text(song.name, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface, maxLines = 1) 273 283 if (song.artist.isNotBlank()) { 274 - Text(song.artist, fontSize = 11.sp, color = MutedText, maxLines = 1) 284 + Text(song.artist, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) 275 285 } 276 286 } 277 287 if (song.isDx) { ··· 283 293 } 284 294 285 295 if (fav.songs.isEmpty()) { 286 - Card( 296 + ExpressiveGlassCard( 287 297 modifier = Modifier.fillMaxWidth(), 288 - shape = RoundedCornerShape(8.dp), 289 - colors = CardDefaults.cardColors(containerColor = CardWhite), 298 + cornerRadius = 24.dp, 290 299 ) { 291 300 Box(Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) { 292 - Text("No favorites yet", fontSize = 14.sp, color = MutedText) 301 + Text("No favorites yet", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 293 302 } 294 303 } 295 304 }
+146 -110
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/FriendsTab.kt
··· 8 8 import androidx.compose.foundation.lazy.LazyColumn 9 9 import androidx.compose.foundation.lazy.items 10 10 import androidx.compose.foundation.rememberScrollState 11 - import androidx.compose.foundation.shape.RoundedCornerShape 12 11 import androidx.compose.material.icons.Icons 13 12 import androidx.compose.material.icons.automirrored.filled.ArrowBack 14 13 import androidx.compose.material.icons.filled.QrCode ··· 16 15 import androidx.compose.material.icons.filled.Search 17 16 import androidx.compose.material.icons.filled.Star 18 17 import androidx.compose.material3.* 19 - import androidx.compose.material3.pulltorefresh.PullToRefreshBox 20 18 import androidx.compose.runtime.* 21 19 import androidx.compose.ui.Alignment 22 20 import androidx.compose.ui.Modifier ··· 42 40 import com.derakkuma.friends.friendScanCode 43 41 import com.derakkuma.friends.friendScanUrl 44 42 import com.derakkuma.ui.components.EmptyCard 43 + import com.derakkuma.ui.components.ExpressiveAssistChip 44 + import com.derakkuma.ui.components.ExpressiveButton 45 + import com.derakkuma.ui.components.ExpressiveFloatingActionButton 46 + import com.derakkuma.ui.components.ExpressiveGlassCard 47 + import com.derakkuma.ui.components.ExpressivePullToRefreshBox 48 + import com.derakkuma.ui.components.ExpressiveFilterChip 49 + import com.derakkuma.ui.components.ExpressiveOutlinedButton 50 + import com.derakkuma.ui.components.ExpressiveTextButton 51 + import com.derakkuma.ui.components.ExpressiveAlertDialog 52 + import com.derakkuma.ui.components.ExpressiveTextField 45 53 import com.derakkuma.ui.components.InfoCard 46 54 import com.derakkuma.ui.components.MaimaiProfile 47 55 import com.derakkuma.ui.components.SectionHeader ··· 405 413 } 406 414 407 415 Box(Modifier.fillMaxSize()) { 408 - PullToRefreshBox( 416 + ExpressivePullToRefreshBox( 409 417 isRefreshing = loading, 410 418 onRefresh = { reload() }, 411 419 modifier = Modifier.fillMaxSize(), ··· 447 455 } 448 456 } 449 457 450 - if (message != null) item { InfoCard(message!!, PrimaryBlue) } 458 + if (message != null) item { InfoCard(message!!, MaterialTheme.colorScheme.primary) } 451 459 if (error != null) item { InfoCard(error!!, MaterialTheme.colorScheme.error) } 452 460 453 461 when (section) { ··· 605 613 circleRewards?.let { page -> 606 614 if (page.rewards.isNotEmpty()) { 607 615 item { SectionHeader("Point Rewards") } 608 - if (page.monthlyIconText.isNotBlank()) item { InfoCard(page.monthlyIconText, PrimaryBlue) } 616 + if (page.monthlyIconText.isNotBlank()) item { InfoCard(page.monthlyIconText, MaterialTheme.colorScheme.primary) } 609 617 items(page.rewards, key = { it.points }) { reward -> RewardRow(reward) } 610 618 } 611 619 } 612 620 } 613 621 614 622 FriendsSection.CircleRanking -> circleRanking?.let { page -> 615 - item { InfoCard("Your circle total: ${page.totalPoints} PT", PrimaryBlue) } 623 + item { InfoCard("Your circle total: ${page.totalPoints} PT", MaterialTheme.colorScheme.primary) } 616 624 items(page.entries, key = { "circle-rank-${it.rank}-${it.name}-${it.points}" }) { entry -> CircleRankingRow(entry) } 617 625 } 618 626 ··· 676 684 } 677 685 } 678 686 679 - FloatingActionButton( 687 + ExpressiveFloatingActionButton( 680 688 onClick = { 681 689 if (mode == FriendsMode.Circles) { 682 690 showCircleSearch = true ··· 696 704 } 697 705 }, 698 706 modifier = Modifier.align(Alignment.BottomEnd).padding(end = 16.dp, bottom = 40.dp), 699 - containerColor = PrimaryBlue, 700 - contentColor = Color.White, 707 + containerColor = MaterialTheme.colorScheme.primary, 708 + contentColor = MaterialTheme.colorScheme.onPrimary, 701 709 ) { 702 710 Icon(Icons.Default.Search, contentDescription = "Search") 703 711 } ··· 706 714 707 715 @Composable 708 716 private fun PrimaryTabs(selected: FriendsMode, onSelect: (FriendsMode) -> Unit) { 709 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 717 + ExpressiveGlassCard(Modifier.fillMaxWidth()) { 710 718 Row(Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { 711 719 FriendsMode.entries.forEach { mode -> 712 - Button( 720 + ExpressiveButton( 713 721 onClick = { onSelect(mode) }, 714 722 modifier = Modifier.weight(1f), 715 723 colors = ButtonDefaults.buttonColors( 716 - containerColor = if (selected == mode) PrimaryBlue else Color(0xFFE5E7EB), 717 - contentColor = if (selected == mode) Color.White else DarkText, 724 + containerColor = if (selected == mode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHigh, 725 + contentColor = if (selected == mode) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface, 718 726 ), 719 727 ) { Text(mode.label) } 720 728 } ··· 731 739 } 732 740 Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(6.dp)) { 733 741 sections.forEach { section -> 734 - FilterChip( 742 + ExpressiveFilterChip( 735 743 selected = selected == section, 736 744 onClick = { onSelect(section) }, 737 745 label = { ··· 740 748 fontSize = 11.sp, 741 749 ) 742 750 }, 743 - colors = FilterChipDefaults.filterChipColors( 744 - containerColor = Color.White.copy(alpha = 0.5f), 745 - selectedContainerColor = Color.White, 746 - ), 747 751 ) 748 752 } 749 753 } ··· 755 759 onAccept: () -> Unit, 756 760 onReject: () -> Unit, 757 761 ) { 758 - Card( 762 + ExpressiveGlassCard( 759 763 Modifier.fillMaxWidth(), 760 - shape = RoundedCornerShape(8.dp), 761 - colors = CardDefaults.cardColors(containerColor = CardWhite), 762 764 ) { 763 765 Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 764 766 UserCard(user = user, expanded = true) 765 767 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 766 - Button(onClick = onAccept, enabled = user.idx.isNotBlank() && user.token.isNotBlank()) { Text("Accept") } 767 - OutlinedButton(onClick = onReject, enabled = user.idx.isNotBlank() && user.token.isNotBlank()) { Text("Reject") } 768 + ExpressiveButton(onClick = onAccept, enabled = user.idx.isNotBlank() && user.token.isNotBlank()) { Text("Accept") } 769 + ExpressiveOutlinedButton(onClick = onReject, enabled = user.idx.isNotBlank() && user.token.isNotBlank()) { Text("Reject") } 768 770 } 769 771 if (user.idx.isBlank() || user.token.isBlank()) { 770 - Text("Can't act on this request yet — missing maimai form token.", fontSize = 11.sp, color = MutedText) 772 + Text("Can't act on this request yet — missing maimai form token.", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 771 773 } 772 774 } 773 775 } ··· 803 805 ratingTextSize = 12.sp, 804 806 showComment = expanded, 805 807 trailingBadges = { 806 - if (user.isFavorite) AssistChip(onClick = {}, label = { Text("Favorite", fontSize = 10.sp) }) 807 - if (user.isRival) AssistChip(onClick = {}, label = { Text("Rival", fontSize = 10.sp) }) 808 + if (user.isFavorite) { 809 + ExpressiveAssistChip( 810 + onClick = {}, 811 + label = { Text("Favorite", fontSize = 10.sp) }, 812 + containerColor = MaterialTheme.colorScheme.tertiaryContainer, 813 + labelColor = MaterialTheme.colorScheme.onTertiaryContainer, 814 + ) 815 + } 816 + if (user.isRival) { 817 + ExpressiveAssistChip( 818 + onClick = {}, 819 + label = { Text("Rival", fontSize = 10.sp) }, 820 + containerColor = MaterialTheme.colorScheme.secondaryContainer, 821 + labelColor = MaterialTheme.colorScheme.onSecondaryContainer, 822 + ) 823 + } 808 824 }, 809 825 details = { 810 - if (user.date.isNotBlank()) Text("Date: ${user.date}", fontSize = 11.sp, color = MutedText) 811 - if (user.status.isNotBlank()) Text(user.status, fontSize = 11.sp, color = MutedText) 826 + if (user.date.isNotBlank()) Text("Date: ${user.date}", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 827 + if (user.status.isNotBlank()) Text(user.status, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 812 828 }, 813 829 actions = { 814 - onVs?.let { ActionButton("Friend VS", Color(0xFF51BCF3), it) } 815 - onFavorite?.let { ActionButton(if (user.isFavorite) "Unfavorite" else "Favorite", Color(0xFFFA8E53), it) } 816 - onRival?.let { ActionButton(if (user.isRival) "Deregister Rival" else "Register Rival", Color(0xFFFB4B78), it) } 817 - if (primaryActionLabel != null && primaryAction != null) Button(onClick = primaryAction) { Text(primaryActionLabel) } 818 - if (secondaryActionLabel != null && secondaryAction != null) OutlinedButton(onClick = secondaryAction) { Text(secondaryActionLabel) } 830 + onVs?.let { ActionButton("Friend VS", Color(0xFF51BCF3), Color.White, it) } 831 + onFavorite?.let { 832 + ActionButton( 833 + if (user.isFavorite) "Unfavorite" else "Favorite", 834 + if (user.isFavorite) Color(0xFFFA8E53) else Color(0xFFFFD7C4), 835 + if (user.isFavorite) Color.White else Color(0xFF8A3A12), 836 + it, 837 + ) 838 + } 839 + onRival?.let { 840 + ActionButton( 841 + if (user.isRival) "Deregister Rival" else "Register Rival", 842 + if (user.isRival) Color(0xFFFB4B78) else Color(0xFFFFD0DC), 843 + if (user.isRival) Color.White else Color(0xFF8E1234), 844 + it, 845 + ) 846 + } 847 + if (primaryActionLabel != null && primaryAction != null) ExpressiveButton(onClick = primaryAction) { Text(primaryActionLabel) } 848 + if (secondaryActionLabel != null && secondaryAction != null) ExpressiveOutlinedButton(onClick = secondaryAction) { Text(secondaryActionLabel) } 819 849 }, 820 850 ) 821 851 } 822 852 823 - @Composable private fun FriendCounts(page: FriendsPage) = InfoCard("Friends ${page.friendCount}/${page.friendMax} \u00B7 Favorites ${page.favoriteCount}/${page.favoriteMax} \u00B7 Rivals ${page.rivalCount}/${page.rivalMax}", PrimaryBlue) 853 + @Composable private fun FriendCounts(page: FriendsPage) = InfoCard("Friends ${page.friendCount}/${page.friendMax} \u00B7 Favorites ${page.favoriteCount}/${page.favoriteMax} \u00B7 Rivals ${page.rivalCount}/${page.rivalMax}", MaterialTheme.colorScheme.primary) 824 854 825 855 @Composable 826 856 private fun FriendQrActions(onScan: () -> Unit, onMyQr: () -> Unit, scanEnabled: Boolean = true) { 827 857 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { 828 - Button( 858 + ExpressiveButton( 829 859 onClick = onScan, 830 860 enabled = scanEnabled, 831 861 modifier = Modifier.weight(1f), 832 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue, contentColor = Color.White), 862 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary), 833 863 ) { 834 864 Icon(Icons.Default.QrCodeScanner, contentDescription = null, modifier = Modifier.size(18.dp)) 835 865 Spacer(Modifier.width(6.dp)) 836 866 Text("Scan to Add") 837 867 } 838 - Button( 868 + ExpressiveButton( 839 869 onClick = onMyQr, 840 870 modifier = Modifier.weight(1f), 841 - colors = ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = PrimaryBlue), 871 + colors = ButtonDefaults.buttonColors( 872 + containerColor = MaterialTheme.colorScheme.secondaryContainer, 873 + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, 874 + ), 842 875 ) { 843 - Icon(Icons.Default.QrCode, contentDescription = null, modifier = Modifier.size(18.dp), tint = PrimaryBlue) 876 + Icon(Icons.Default.QrCode, contentDescription = null, modifier = Modifier.size(18.dp)) 844 877 Spacer(Modifier.width(6.dp)) 845 878 Text("My Friend QR") 846 879 } ··· 848 881 } 849 882 850 883 @Composable 851 - private fun ActionButton(text: String, color: Color, onClick: () -> Unit) { 852 - Button( 884 + private fun ActionButton(text: String, color: Color, contentColor: Color, onClick: () -> Unit) { 885 + ExpressiveButton( 853 886 onClick = onClick, 854 - colors = ButtonDefaults.buttonColors(containerColor = color, contentColor = Color.White), 887 + colors = ButtonDefaults.buttonColors(containerColor = color, contentColor = contentColor), 855 888 contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp), 856 889 ) { 857 890 Text(text, fontSize = 11.sp) ··· 860 893 861 894 @Composable 862 895 private fun FriendCodeCard(friendCode: FriendCode) { 863 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 896 + ExpressiveGlassCard(Modifier.fillMaxWidth()) { 864 897 Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { 865 - Text("My Friend Code", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = DarkText) 898 + Text("My Friend Code", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 866 899 Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { 867 - Text(friendCode.code, fontSize = 22.sp, fontWeight = FontWeight.ExtraBold, color = PrimaryBlue) 900 + Text(friendCode.code, fontSize = 22.sp, fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.primary) 868 901 CopyButton(friendCode.code) 869 902 } 870 - if (friendCode.name.isNotBlank()) Text(friendCode.name, fontSize = 13.sp, color = MutedText) 903 + if (friendCode.name.isNotBlank()) Text(friendCode.name, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 871 904 } 872 905 } 873 906 } 874 907 875 908 @Composable 876 909 private fun CompactFriendCodeCard(friendCode: FriendCode) { 877 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 910 + ExpressiveGlassCard(Modifier.fillMaxWidth()) { 878 911 Row(Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { 879 912 Column { 880 - Text("My Friend Code", fontSize = 11.sp, color = MutedText) 881 - Text(friendCode.code, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = PrimaryBlue) 913 + Text("My Friend Code", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 914 + Text(friendCode.code, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) 882 915 } 883 916 CopyButton(friendCode.code) 884 917 } ··· 887 920 888 921 @Composable 889 922 private fun SearchFriendCard(code: String, onCode: (String) -> Unit, onSearch: () -> Unit) { 890 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 923 + ExpressiveGlassCard(Modifier.fillMaxWidth()) { 891 924 Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 892 - OutlinedTextField(value = code, onValueChange = onCode, label = { Text("Friend Code") }, modifier = Modifier.fillMaxWidth(), singleLine = true) 893 - Button(onClick = onSearch, enabled = code.isNotBlank()) { Text("Search") } 925 + ExpressiveTextField( 926 + value = code, 927 + onValueChange = onCode, 928 + label = { Text("Friend Code") }, 929 + modifier = Modifier.fillMaxWidth(), 930 + singleLine = true, 931 + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, 932 + ) 933 + ExpressiveButton(onClick = onSearch, enabled = code.isNotBlank()) { Text("Search") } 894 934 } 895 935 } 896 936 } ··· 898 938 @Composable 899 939 private fun FriendQrScannerDialog(onDismiss: () -> Unit, onScanned: (String) -> Unit, onFailure: (String) -> Unit) { 900 940 Dialog(onDismissRequest = onDismiss) { 901 - Card(Modifier.fillMaxWidth().height(460.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 941 + ExpressiveGlassCard(Modifier.fillMaxWidth().height(460.dp), cornerRadius = 32.dp) { 902 942 Box(Modifier.fillMaxSize()) { 903 943 ScannerWithPermissions( 904 944 modifier = Modifier.fillMaxSize(), ··· 910 950 cameraPosition = CameraPosition.BACK, 911 951 enableTorch = false, 912 952 ) 913 - TextButton(onClick = onDismiss, modifier = Modifier.align(Alignment.TopEnd).padding(8.dp)) { 914 - Text("Close", color = Color.White) 953 + ExpressiveTextButton(onClick = onDismiss, modifier = Modifier.align(Alignment.TopEnd).padding(8.dp)) { 954 + Text("Close", color = MaterialTheme.colorScheme.onPrimary) 915 955 } 916 956 } 917 957 } ··· 945 985 } 946 986 } 947 987 } 948 - AlertDialog( 988 + ExpressiveAlertDialog( 949 989 onDismissRequest = onDismiss, 950 990 title = { Text("My Friend QR") }, 951 991 text = { 952 992 Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) { 953 - Card(shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = Color.White)) { 993 + ExpressiveGlassCard(cornerRadius = 28.dp, containerColor = Color.White) { 954 994 Box(Modifier.padding(14.dp), contentAlignment = Alignment.Center) { 955 995 Image( 956 996 painter = painter, ··· 959 999 ) 960 1000 } 961 1001 } 962 - Text(friendCode.code, fontSize = 20.sp, fontWeight = FontWeight.ExtraBold, color = PrimaryBlue) 963 - friendCode.name.takeIf { it.isNotBlank() }?.let { Text(it, fontSize = 13.sp, color = MutedText) } 1002 + Text(friendCode.code, fontSize = 20.sp, fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.primary) 1003 + friendCode.name.takeIf { it.isNotBlank() }?.let { Text(it, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) } 964 1004 } 965 1005 }, 966 - confirmButton = { TextButton(onClick = onDismiss) { Text("Done", color = PrimaryBlue) } }, 1006 + confirmButton = { ExpressiveTextButton(onClick = onDismiss) { Text("Done", color = MaterialTheme.colorScheme.primary) } }, 967 1007 ) 968 1008 } 969 1009 ··· 976 1016 onDismiss: () -> Unit, 977 1017 onFetchToken: () -> Unit, 978 1018 ) { 979 - AlertDialog( 1019 + ExpressiveAlertDialog( 980 1020 onDismissRequest = onDismiss, 981 1021 title = { Text("Search Circles") }, 982 1022 text = { 983 1023 Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { 984 - OutlinedTextField( 1024 + ExpressiveTextField( 985 1025 value = code, 986 1026 onValueChange = onCode, 987 1027 modifier = Modifier.fillMaxWidth(), 988 1028 label = { Text("Circle code") }, 989 1029 singleLine = true, 990 - ) 991 - page?.message?.takeIf { it.isNotBlank() }?.let { Text(it, color = MutedText, fontSize = 13.sp) } 1030 + ) 1031 + page?.message?.takeIf { it.isNotBlank() }?.let { Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp) } 992 1032 } 993 1033 }, 994 1034 confirmButton = { 995 - TextButton( 1035 + ExpressiveTextButton( 996 1036 onClick = onSearch, 997 1037 enabled = code.isNotBlank() && page?.token?.isNotBlank() == true, 998 - ) { Text("Search", color = PrimaryBlue) } 1038 + ) { Text("Search", color = MaterialTheme.colorScheme.primary) } 999 1039 }, 1000 1040 dismissButton = { 1001 - TextButton(onClick = onDismiss) { Text("Cancel") } 1041 + ExpressiveTextButton(onClick = onDismiss) { Text("Cancel") } 1002 1042 }, 1003 1043 ) 1004 1044 } 1005 1045 1006 1046 @Composable 1007 1047 private fun CircleHomeCard(home: CircleHome, onNavigate: (FriendsSection) -> Unit = {}) { 1008 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 1048 + ExpressiveGlassCard(Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 1009 1049 Box { 1010 1050 MaimaiRemoteImage(home.backgroundImageUrl, modifier = Modifier.fillMaxWidth().height(170.dp), contentDescription = null, contentScale = ContentScale.Crop) 1011 - Box(Modifier.matchParentSize().background(Color.White.copy(alpha = 0.74f))) 1051 + Box(Modifier.matchParentSize().background(MaterialTheme.colorScheme.surface.copy(alpha = 0.74f))) 1012 1052 Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 1013 1053 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { 1014 - MaimaiRemoteImage(home.characterImageUrl ?: home.backgroundImageUrl, modifier = Modifier.size(64.dp).clip(RoundedCornerShape(8.dp)), contentDescription = home.name, contentScale = ContentScale.Crop) 1054 + MaimaiRemoteImage(home.characterImageUrl ?: home.backgroundImageUrl, modifier = Modifier.size(64.dp).clip(MaterialTheme.shapes.large), contentDescription = home.name, contentScale = ContentScale.Crop) 1015 1055 Column(Modifier.weight(1f)) { 1016 - Text(home.name.ifBlank { "Circle" }, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText) 1017 - if (home.ownerName.isNotBlank()) Text(home.ownerName, color = MutedText, fontSize = 12.sp) 1018 - if (home.comment.isNotBlank()) Text(home.comment, color = MutedText, fontSize = 12.sp) 1056 + Text(home.name.ifBlank { "Circle" }, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 1057 + if (home.ownerName.isNotBlank()) Text(home.ownerName, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 1058 + if (home.comment.isNotBlank()) Text(home.comment, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 1019 1059 } 1020 1060 } 1021 1061 if (home.code.isNotBlank()) { 1022 1062 Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { 1023 - Text("Circle Code: ${home.code}", color = PrimaryBlue, fontWeight = FontWeight.SemiBold) 1063 + Text("Circle Code: ${home.code}", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.SemiBold) 1024 1064 CopyButton(home.code) 1025 1065 } 1026 1066 } 1027 1067 if (home.tags.isNotEmpty()) { 1028 - Text(home.tags.joinToString(" \u00B7 "), color = MutedText, fontSize = 11.sp) 1068 + Text(home.tags.joinToString(" \u00B7 "), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 1029 1069 } 1030 - Text("${home.month}: ${home.totalPoints} PT", color = PrimaryBlue, fontWeight = FontWeight.SemiBold) 1031 - Text("Rank ${home.rank} \u00B7 ${home.daysUntilReset} days until reset", color = MutedText, fontSize = 12.sp) 1032 - if (home.updatedAt.isNotBlank()) Text("Updated: ${home.updatedAt}", color = MutedText, fontSize = 12.sp) 1033 - if (home.nextRewardPoints > 0) Text("Next reward: ${home.nextRewardPoints} PT", color = DarkText) 1034 - if (home.festaMessage.isNotBlank()) Text(home.festaMessage, color = MutedText, fontSize = 12.sp) 1070 + Text("${home.month}: ${home.totalPoints} PT", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.SemiBold) 1071 + Text("Rank ${home.rank} \u00B7 ${home.daysUntilReset} days until reset", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 1072 + if (home.updatedAt.isNotBlank()) Text("Updated: ${home.updatedAt}", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 1073 + if (home.nextRewardPoints > 0) Text("Next reward: ${home.nextRewardPoints} PT", color = MaterialTheme.colorScheme.onSurface) 1074 + if (home.festaMessage.isNotBlank()) Text(home.festaMessage, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 1035 1075 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 1036 - TextButton(onClick = { onNavigate(FriendsSection.CircleRanking) }) { Text("Ranking") } 1037 - TextButton(onClick = { onNavigate(FriendsSection.CircleAccept) }) { Text("Invites") } 1038 - if (home.festaMessage.isNotBlank()) TextButton(onClick = { onNavigate(FriendsSection.Festa) }) { Text("FESTA") } 1076 + ExpressiveTextButton(onClick = { onNavigate(FriendsSection.CircleRanking) }) { Text("Ranking") } 1077 + ExpressiveTextButton(onClick = { onNavigate(FriendsSection.CircleAccept) }) { Text("Invites") } 1078 + if (home.festaMessage.isNotBlank()) ExpressiveTextButton(onClick = { onNavigate(FriendsSection.Festa) }) { Text("FESTA") } 1039 1079 } 1040 1080 } 1041 1081 } 1042 1082 } 1043 1083 } 1044 1084 1045 - @Composable private fun RewardRow(reward: CircleReward) = InfoCard("${reward.points} PT \u2014 ${reward.reward}", DarkText) 1085 + @Composable private fun RewardRow(reward: CircleReward) = InfoCard("${reward.points} PT \u2014 ${reward.reward}", MaterialTheme.colorScheme.onSurface) 1046 1086 1047 - @Composable private fun CircleRankingRow(entry: CircleRankingEntry) = InfoCard("#${entry.rank} ${entry.name} \u2014 ${entry.points} PT", DarkText) 1087 + @Composable private fun CircleRankingRow(entry: CircleRankingEntry) = InfoCard("#${entry.rank} ${entry.name} \u2014 ${entry.points} PT", MaterialTheme.colorScheme.onSurface) 1048 1088 1049 - @Composable private fun CircleMemberRow(member: CircleMember) = InfoCard("${member.name} \u00B7 Rating ${member.rating} \u00B7 ${member.points} PT${if (member.status.isNotBlank()) " \u00B7 ${member.status}" else ""}", DarkText) 1089 + @Composable private fun CircleMemberRow(member: CircleMember) = InfoCard("${member.name} \u00B7 Rating ${member.rating} \u00B7 ${member.points} PT${if (member.status.isNotBlank()) " \u00B7 ${member.status}" else ""}", MaterialTheme.colorScheme.onSurface) 1050 1090 1051 1091 @Composable 1052 1092 private fun FestaNav(selected: FestaSection, onSelect: (FestaSection) -> Unit) { 1053 1093 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 1054 1094 FestaSection.entries.forEach { section -> 1055 - FilterChip( 1095 + ExpressiveFilterChip( 1056 1096 selected = selected == section, 1057 1097 onClick = { onSelect(section) }, 1058 1098 label = { Text(section.label) }, 1059 - colors = FilterChipDefaults.filterChipColors( 1060 - containerColor = Color.White.copy(alpha = 0.5f), 1061 - selectedContainerColor = Color.White, 1062 - ), 1063 1099 ) 1064 1100 } 1065 1101 } ··· 1067 1103 1068 1104 @Composable 1069 1105 private fun FriendVsCard(vs: FriendVsPage) { 1070 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 1106 + ExpressiveGlassCard(Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 1071 1107 Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 1072 - Text("Friend VS", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = DarkText) 1108 + Text("Friend VS", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 1073 1109 if (vs.playerName.isNotBlank() || vs.friendName.isNotBlank()) { 1074 1110 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) { 1075 1111 Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.weight(1f)) { 1076 - MaimaiRemoteImage(vs.playerIconUrl, modifier = Modifier.size(44.dp).clip(RoundedCornerShape(6.dp)), contentDescription = vs.playerName, contentScale = ContentScale.Crop) 1112 + MaimaiRemoteImage(vs.playerIconUrl, modifier = Modifier.size(44.dp).clip(MaterialTheme.shapes.large), contentDescription = vs.playerName, contentScale = ContentScale.Crop) 1077 1113 Spacer(Modifier.height(4.dp)) 1078 - Text(vs.playerName.ifBlank { "You" }, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = DarkText, maxLines = 1) 1079 - if (vs.playerRating > 0) Text("${vs.playerRating}", fontSize = 15.sp, fontWeight = FontWeight.ExtraBold, color = PrimaryBlue) 1114 + Text(vs.playerName.ifBlank { "You" }, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, maxLines = 1) 1115 + if (vs.playerRating > 0) Text("${vs.playerRating}", fontSize = 15.sp, fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.primary) 1080 1116 } 1081 - Text("VS", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MutedText) 1117 + Text("VS", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurfaceVariant) 1082 1118 Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.weight(1f)) { 1083 - MaimaiRemoteImage(vs.friendIconUrl, modifier = Modifier.size(44.dp).clip(RoundedCornerShape(6.dp)), contentDescription = vs.friendName, contentScale = ContentScale.Crop) 1119 + MaimaiRemoteImage(vs.friendIconUrl, modifier = Modifier.size(44.dp).clip(MaterialTheme.shapes.large), contentDescription = vs.friendName, contentScale = ContentScale.Crop) 1084 1120 Spacer(Modifier.height(4.dp)) 1085 - Text(vs.friendName.ifBlank { "Friend" }, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = DarkText, maxLines = 1) 1086 - if (vs.friendRating > 0) Text("${vs.friendRating}", fontSize = 15.sp, fontWeight = FontWeight.ExtraBold, color = Color(0xFFFB4B78)) 1121 + Text(vs.friendName.ifBlank { "Friend" }, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, maxLines = 1) 1122 + if (vs.friendRating > 0) Text("${vs.friendRating}", fontSize = 15.sp, fontWeight = FontWeight.ExtraBold, color = MaterialTheme.colorScheme.secondary) 1087 1123 } 1088 1124 } 1089 1125 } 1090 - if (vs.message.isNotBlank()) Text(vs.message, color = MutedText, fontSize = 12.sp) 1091 - if (vs.genres.isNotEmpty()) Text("Genres: ${vs.genres.joinToString()}", color = MutedText, fontSize = 11.sp) 1092 - if (vs.difficulties.isNotEmpty()) Text("Difficulties: ${vs.difficulties.joinToString()}", color = MutedText, fontSize = 11.sp) 1093 - if (vs.scoreTypes.isNotEmpty()) Text("Score types: ${vs.scoreTypes.joinToString()}", color = MutedText, fontSize = 11.sp) 1126 + if (vs.message.isNotBlank()) Text(vs.message, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) 1127 + if (vs.genres.isNotEmpty()) Text("Genres: ${vs.genres.joinToString()}", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 1128 + if (vs.difficulties.isNotEmpty()) Text("Difficulties: ${vs.difficulties.joinToString()}", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 1129 + if (vs.scoreTypes.isNotEmpty()) Text("Score types: ${vs.scoreTypes.joinToString()}", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp) 1094 1130 } 1095 1131 } 1096 1132 } 1097 1133 1098 1134 @Composable 1099 1135 private fun FestaHistoryRow(history: CircleFestaHistory) { 1100 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 1136 + ExpressiveGlassCard(Modifier.fillMaxWidth(), cornerRadius = 24.dp) { 1101 1137 Row(Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { 1102 - MaimaiRemoteImage(history.imageUrl, modifier = Modifier.size(64.dp).clip(RoundedCornerShape(8.dp)), contentDescription = history.season, contentScale = ContentScale.Crop) 1138 + MaimaiRemoteImage(history.imageUrl, modifier = Modifier.size(64.dp).clip(MaterialTheme.shapes.large), contentDescription = history.season, contentScale = ContentScale.Crop) 1103 1139 Column(Modifier.weight(1f)) { 1104 - Text(history.season.ifBlank { "Past FESTA" }, fontSize = 15.sp, fontWeight = FontWeight.Bold, color = DarkText) 1105 - if (history.period.isNotBlank()) Text(history.period, fontSize = 12.sp, color = MutedText) 1106 - if (history.team.isNotBlank()) Text(history.team, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFFFB4B78)) 1140 + Text(history.season.ifBlank { "Past FESTA" }, fontSize = 15.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 1141 + if (history.period.isNotBlank()) Text(history.period, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 1142 + if (history.team.isNotBlank()) Text(history.team, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.secondary) 1107 1143 } 1108 1144 } 1109 1145 } 1110 1146 } 1111 1147 1112 - @Composable private fun BackButton(text: String, onClick: () -> Unit) = TextButton(onClick = onClick) { 1148 + @Composable private fun BackButton(text: String, onClick: () -> Unit) = ExpressiveTextButton(onClick = onClick) { 1113 1149 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 1114 1150 Text(text) 1115 1151 }
+31 -31
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/HomeTab.kt
··· 3 3 import androidx.compose.foundation.clickable 4 4 import androidx.compose.foundation.layout.* 5 5 import androidx.compose.foundation.rememberScrollState 6 - import androidx.compose.foundation.shape.RoundedCornerShape 7 6 import androidx.compose.foundation.verticalScroll 8 7 import androidx.compose.material.icons.Icons 9 8 import androidx.compose.material.icons.filled.Visibility 10 9 import androidx.compose.material.icons.filled.VisibilityOff 11 10 import androidx.compose.material3.* 12 - import androidx.compose.material3.pulltorefresh.PullToRefreshBox 13 11 import androidx.compose.runtime.* 14 12 import androidx.compose.ui.Alignment 15 13 import androidx.compose.ui.Modifier ··· 25 23 import com.derakkuma.data.RatingData 26 24 import com.derakkuma.ui.components.BigStat 27 25 import com.derakkuma.ui.components.ErrorState 26 + import com.derakkuma.ui.components.ExpressiveGlassCard 27 + import com.derakkuma.ui.components.ExpressivePullToRefreshBox 28 + import com.derakkuma.ui.components.ExpressiveLinearProgressIndicator 29 + import com.derakkuma.ui.components.ExpressiveTextButton 28 30 import com.derakkuma.ui.components.LoadingState 29 31 import com.derakkuma.ui.components.MaimaiProfile 30 32 import com.derakkuma.ui.components.PlayHeatmap ··· 134 136 } 135 137 } 136 138 137 - PullToRefreshBox( 139 + ExpressivePullToRefreshBox( 138 140 isRefreshing = isRefreshing, 139 141 onRefresh = { 140 - if (maintenanceMode) return@PullToRefreshBox 142 + if (maintenanceMode) return@ExpressivePullToRefreshBox 141 143 isRefreshing = true 142 144 refreshKey += 1 143 145 }, ··· 147 149 ) { 148 150 // Thin loading bar at top when refreshing 149 151 if (loading && data != null) { 150 - LinearProgressIndicator( 152 + ExpressiveLinearProgressIndicator( 151 153 modifier = Modifier.fillMaxWidth().height(2.dp), 152 - color = PrimaryBlue, 154 + color = MaterialTheme.colorScheme.primary, 153 155 trackColor = Color.Transparent, 154 156 ) 155 157 } ··· 191 193 // Missions 192 194 if (pd.missions.tasks.isNotEmpty() || pd.missions.deadline.isNotBlank()) { 193 195 Spacer(Modifier.height(8.dp)) 194 - Card( 196 + ExpressiveGlassCard( 195 197 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp), 196 - shape = RoundedCornerShape(8.dp), 197 - colors = CardDefaults.cardColors(containerColor = CardWhite), 198 + cornerRadius = 28.dp, 198 199 ) { 199 200 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 200 - Text("Missions", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 201 + Text("Missions", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 201 202 if (pd.missions.deadline.isNotBlank()) { 202 - Text("Until ${pd.missions.deadline}", fontSize = 12.sp, color = MutedText) 203 + Text("Until ${pd.missions.deadline}", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 203 204 } 204 - Text("Progress: ${pd.missions.progress}", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue) 205 + Text("Progress: ${pd.missions.progress}", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) 205 206 pd.missions.tasks.forEach { task -> 206 207 Row( 207 208 modifier = Modifier.fillMaxWidth(), 208 209 horizontalArrangement = Arrangement.SpaceBetween, 209 210 ) { 210 - Text(task.description, fontSize = 13.sp, color = DarkText, modifier = Modifier.weight(1f)) 211 - Text("+${task.reward}", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = AccentPink) 211 + Text(task.description, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f)) 212 + Text("+${task.reward}", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.secondary) 212 213 } 213 214 } 214 215 } ··· 236 237 237 238 @Composable 238 239 private fun LastPlayedCard(play: RecentPlay, onClick: (() -> Unit)? = null) { 239 - Card( 240 + ExpressiveGlassCard( 240 241 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp) 241 242 .clickable(enabled = onClick != null) { onClick?.invoke() }, 242 - shape = RoundedCornerShape(8.dp), 243 - colors = CardDefaults.cardColors(containerColor = CardWhite), 243 + cornerRadius = 24.dp, 244 244 ) { 245 245 Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 246 246 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { 247 - Text("Last played", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 248 - if (onClick != null) Text("Tap for details", fontSize = 11.sp, color = PrimaryBlue, fontWeight = FontWeight.SemiBold) 247 + Text("Last played", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 248 + if (onClick != null) Text("Tap for details", fontSize = 11.sp, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.SemiBold) 249 249 } 250 250 SongRow( 251 251 name = play.name, ··· 269 269 270 270 @Composable 271 271 private fun StatusChip(label: String) { 272 - Card( 273 - shape = RoundedCornerShape(8.dp), 274 - colors = CardDefaults.cardColors(containerColor = PrimaryBlue.copy(alpha = 0.1f)), 272 + ExpressiveGlassCard( 273 + cornerRadius = 18.dp, 274 + containerColor = MaterialTheme.colorScheme.primaryContainer, 275 275 ) { 276 276 Row( 277 277 modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), 278 278 horizontalArrangement = Arrangement.spacedBy(6.dp), 279 279 verticalAlignment = Alignment.CenterVertically, 280 280 ) { 281 - Text(label, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue) 282 - Text("!", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = AccentPink) 281 + Text(label, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onPrimaryContainer) 282 + Text("!", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.secondary) 283 283 } 284 284 } 285 285 } ··· 307 307 ratingTrailing = { 308 308 Spacer(Modifier.weight(1f)) 309 309 if (friendCode != null) { 310 - TextButton(onClick = onToggleFriendCode) { 310 + ExpressiveTextButton(onClick = onToggleFriendCode) { 311 311 Icon( 312 312 imageVector = if (showFriendCode) Icons.Default.Visibility else Icons.Default.VisibilityOff, 313 313 contentDescription = if (showFriendCode) "Hide friend code" else "Show friend code", 314 - tint = PrimaryBlue, 314 + tint = MaterialTheme.colorScheme.primary, 315 315 modifier = Modifier.size(20.dp), 316 316 ) 317 317 Spacer(Modifier.width(4.dp)) 318 318 Text( 319 319 text = "Friend Code", 320 320 fontSize = 12.sp, 321 - color = PrimaryBlue, 321 + color = MaterialTheme.colorScheme.primary, 322 322 ) 323 323 } 324 324 } 325 325 }, 326 326 details = { 327 327 if (friendCode != null && showFriendCode) { 328 - Card( 328 + ExpressiveGlassCard( 329 329 modifier = Modifier.fillMaxWidth(), 330 - shape = RoundedCornerShape(6.dp), 331 - colors = CardDefaults.cardColors(containerColor = Color(0xFFF3F4F6)), 330 + cornerRadius = 16.dp, 331 + containerColor = Color.White, 332 332 ) { 333 333 Row( 334 334 modifier = Modifier.fillMaxWidth().padding(12.dp), ··· 339 339 text = friendCode.code, 340 340 fontSize = 20.sp, 341 341 fontWeight = FontWeight.Bold, 342 - color = DarkText, 342 + color = MaterialTheme.colorScheme.onSurface, 343 343 ) 344 344 CopyButton(friendCode.code) 345 345 }
+24 -24
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/KamaitachiSettingsTab.kt
··· 1 1 package com.derakkuma.ui.tabs 2 2 3 + import com.derakkuma.ui.components.ExpressiveLinearProgressIndicator 3 4 import androidx.compose.foundation.layout.* 4 5 import androidx.compose.foundation.rememberScrollState 5 - import androidx.compose.foundation.shape.RoundedCornerShape 6 6 import androidx.compose.foundation.verticalScroll 7 7 import androidx.compose.material.icons.Icons 8 8 import androidx.compose.material.icons.filled.Link ··· 16 16 import com.derakkuma.data.PlayStore 17 17 import com.derakkuma.kamaitachi.KamaitachiClient 18 18 import com.derakkuma.kamaitachi.KamaitachiSyncProgress 19 + import com.derakkuma.ui.components.ExpressiveButton 20 + import com.derakkuma.ui.components.ExpressiveGlassCard 21 + import com.derakkuma.ui.components.ExpressiveSnackbar 22 + import com.derakkuma.ui.components.ExpressiveLoadingIndicator 23 + import com.derakkuma.ui.components.ExpressiveScaffold 24 + import com.derakkuma.ui.components.ExpressiveOutlinedButton 19 25 import com.derakkuma.ui.theme.* 20 26 import kotlinx.coroutines.launch 21 27 ··· 29 35 var snackMsg by remember { mutableStateOf<String?>(null) } 30 36 val scope = rememberCoroutineScope() 31 37 32 - Scaffold( 38 + ExpressiveScaffold( 33 39 contentWindowInsets = WindowInsets(0.dp), 34 40 snackbarHost = { 35 41 if (snackMsg != null) { 36 - Snackbar( 42 + ExpressiveSnackbar( 37 43 modifier = Modifier.padding(12.dp), 38 - shape = RoundedCornerShape(8.dp), 39 - action = { TextButton(onClick = { snackMsg = null }) { Text("OK") } }, 44 + action = { com.derakkuma.ui.components.ExpressiveTextButton(onClick = { snackMsg = null }) { Text("OK") } }, 40 45 ) { Text(snackMsg!!) } 41 46 } 42 47 }, ··· 49 54 .padding(start = 12.dp, end = 12.dp, bottom = 88.dp), 50 55 verticalArrangement = Arrangement.spacedBy(8.dp), 51 56 ) { 52 - Text("Kamaitachi", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(bottom = 4.dp)) 57 + Text("Kamaitachi", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 4.dp)) 53 58 54 - Card( 59 + ExpressiveGlassCard( 55 60 modifier = Modifier.fillMaxWidth(), 56 - shape = RoundedCornerShape(8.dp), 57 - colors = CardDefaults.cardColors(containerColor = CardWhite), 58 61 ) { 59 62 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { 60 - Text("Score Sync", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 61 - Text("Sync your recent and archived maimai DX records to Kamaitachi.", fontSize = 12.sp, color = MutedText) 63 + Text("Score Sync", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 64 + Text("Sync your recent and archived maimai DX records to Kamaitachi.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 62 65 63 66 var syncing by remember { mutableStateOf(false) } 64 67 var syncProgress by remember { mutableStateOf<KamaitachiSyncProgress?>(null) } 65 68 66 69 if (kamaitachiClient.lastStatus.isNotBlank()) { 67 - Text(kamaitachiClient.lastStatus, fontSize = 12.sp, color = MutedText) 70 + Text(kamaitachiClient.lastStatus, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 68 71 } 69 72 if (syncing && syncProgress != null) { 70 73 val progress = syncProgress!!.current.toFloat() / syncProgress!!.total.coerceAtLeast(1).toFloat() 71 - LinearProgressIndicator( 74 + ExpressiveLinearProgressIndicator( 72 75 progress = { progress.coerceIn(0f, 1f) }, 73 76 modifier = Modifier.fillMaxWidth(), 74 - color = PrimaryBlue, 77 + color = MaterialTheme.colorScheme.primary, 75 78 ) 76 - Text(syncProgress!!.status, fontSize = 12.sp, color = MutedText) 79 + Text(syncProgress!!.status, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 77 80 } 78 81 79 82 if (kamaitachiClient.isLinked) { 80 83 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { 81 - Button( 84 + ExpressiveButton( 82 85 onClick = { 83 86 syncing = true 84 87 syncProgress = null ··· 99 102 } 100 103 }, 101 104 modifier = Modifier.weight(1f), 102 - shape = RoundedCornerShape(8.dp), 103 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), 105 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 104 106 enabled = !syncing, 105 107 ) { 106 108 if (syncing) { 107 - CircularProgressIndicator(modifier = Modifier.size(16.dp), color = CardWhite, strokeWidth = 2.dp) 109 + ExpressiveLoadingIndicator(modifier = Modifier, color = MaterialTheme.colorScheme.onPrimary, size = 16.dp) 108 110 Spacer(Modifier.width(8.dp)) 109 111 } 110 112 Text(if (syncing) "Syncing..." else "Sync Records", fontWeight = FontWeight.SemiBold) 111 113 } 112 - OutlinedButton( 114 + ExpressiveOutlinedButton( 113 115 onClick = { 114 116 kamaitachiClient.disconnect() 115 117 snackMsg = "Kamaitachi disconnected" 116 118 }, 117 - shape = RoundedCornerShape(8.dp), 118 119 enabled = !syncing, 119 120 ) { 120 121 Text("Disconnect") 121 122 } 122 123 } 123 124 } else { 124 - Button( 125 + ExpressiveButton( 125 126 onClick = { kamaitachiClient.beginLogin() }, 126 127 modifier = Modifier.fillMaxWidth(), 127 - shape = RoundedCornerShape(8.dp), 128 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), 128 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 129 129 ) { 130 130 Icon(Icons.Default.Link, contentDescription = null, modifier = Modifier.size(18.dp)) 131 131 Spacer(Modifier.width(8.dp))
+21 -19
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/MapTab.kt
··· 4 4 import androidx.compose.foundation.layout.* 5 5 import androidx.compose.foundation.lazy.LazyColumn 6 6 import androidx.compose.foundation.lazy.items 7 - import androidx.compose.foundation.shape.RoundedCornerShape 8 7 import androidx.compose.material.icons.Icons 9 8 import androidx.compose.material.icons.outlined.LocationOn 10 9 import androidx.compose.material3.* ··· 21 20 import com.derakkuma.data.distanceKm 22 21 import com.derakkuma.location.UserLocationProvider 23 22 import com.derakkuma.maps.openMapLocation 24 - import com.derakkuma.ui.theme.CardWhite 25 - import com.derakkuma.ui.theme.DarkText 26 - import com.derakkuma.ui.theme.MutedText 27 - import com.derakkuma.ui.theme.PrimaryBlue 23 + import com.derakkuma.ui.components.ExpressiveGlassCard 24 + import com.derakkuma.ui.components.ExpressiveTextButton 25 + import com.derakkuma.ui.components.ExpressiveTopLoadingIndicator 28 26 import com.swmansion.kmpmaps.core.CameraPosition 29 27 import com.swmansion.kmpmaps.core.ClusterSettings 30 28 import com.swmansion.kmpmaps.core.Coordinates ··· 77 75 Column(Modifier.fillMaxSize().padding(start = 12.dp, end = 12.dp, top = 12.dp)) { 78 76 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { 79 77 Column { 80 - Text("Cab Map", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText) 81 - Text(locationStatus(userLocation, locations.size), fontSize = 12.sp, color = MutedText) 78 + Text("Cab Map", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 79 + Text(locationStatus(userLocation, locations.size), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 82 80 } 83 - TextButton(onClick = { refresh(force = true) }, enabled = !loading) { 81 + ExpressiveTextButton(onClick = { refresh(force = true) }, enabled = !loading) { 84 82 Text("Refresh") 85 83 } 86 84 } 87 85 88 86 Spacer(Modifier.height(8.dp)) 89 87 90 - Card(Modifier.fillMaxWidth().height(260.dp), shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 88 + if (loading) { 89 + ExpressiveTopLoadingIndicator(color = MaterialTheme.colorScheme.primary, trackColor = androidx.compose.ui.graphics.Color.Transparent) 90 + Spacer(Modifier.height(6.dp)) 91 + } 92 + 93 + ExpressiveGlassCard(Modifier.fillMaxWidth().height(260.dp), cornerRadius = 28.dp) { 91 94 if (locations.isEmpty() && loading) { 92 - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator(color = PrimaryBlue) } 95 + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Loading arcade locations…", color = MaterialTheme.colorScheme.onSurfaceVariant) } 93 96 } else if (locations.isEmpty()) { 94 97 Box(Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) { 95 - Text(error ?: "No locations loaded yet", color = MutedText) 98 + Text(error ?: "No locations loaded yet", color = MaterialTheme.colorScheme.onSurfaceVariant) 96 99 } 97 100 } else { 98 101 Map( ··· 114 117 } 115 118 116 119 if (error != null && locations.isNotEmpty()) { 117 - Text("using cached locations: $error", modifier = Modifier.padding(top = 8.dp), fontSize = 12.sp, color = MutedText) 120 + Text("using cached locations: $error", modifier = Modifier.padding(top = 8.dp), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 118 121 } 119 122 120 123 Spacer(Modifier.height(8.dp)) ··· 129 132 130 133 @Composable 131 134 private fun ArcadeLocationRow(location: ArcadeLocation, userLocation: UserLocation?) { 132 - Card( 135 + ExpressiveGlassCard( 133 136 Modifier.fillMaxWidth().clickable { openMapLocation(location.name, location.latitude, location.longitude, location.mapsUrl) }, 134 - shape = RoundedCornerShape(8.dp), 135 - colors = CardDefaults.cardColors(containerColor = CardWhite), 137 + cornerRadius = 24.dp, 136 138 ) { 137 139 Row(Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Top) { 138 - Icon(Icons.Outlined.LocationOn, contentDescription = null, tint = PrimaryBlue, modifier = Modifier.size(22.dp)) 140 + Icon(Icons.Outlined.LocationOn, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(22.dp)) 139 141 Column(Modifier.weight(1f)) { 140 - Text(location.name, fontWeight = FontWeight.Bold, color = DarkText, fontSize = 14.sp) 141 - if (location.address.isNotBlank()) Text(location.address, color = MutedText, fontSize = 12.sp, lineHeight = 15.sp) 142 + Text(location.name, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp) 143 + if (location.address.isNotBlank()) Text(location.address, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, lineHeight = 15.sp) 142 144 } 143 145 userLocation?.let { 144 - Text(formatDistance(distanceKm(it, location)), color = PrimaryBlue, fontWeight = FontWeight.Bold, fontSize = 12.sp) 146 + Text(formatDistance(distanceKm(it, location)), color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) 145 147 } 146 148 } 147 149 }
+57 -31
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/MoreTab.kt
··· 2 2 3 3 import androidx.compose.animation.core.LinearEasing 4 4 import androidx.compose.animation.core.animateFloat 5 + import androidx.compose.animation.core.animateFloatAsState 5 6 import androidx.compose.animation.core.infiniteRepeatable 6 7 import androidx.compose.animation.core.rememberInfiniteTransition 8 + import androidx.compose.animation.core.spring 7 9 import androidx.compose.animation.core.tween 8 10 import androidx.compose.foundation.clickable 11 + import androidx.compose.foundation.interaction.MutableInteractionSource 12 + import androidx.compose.foundation.interaction.collectIsPressedAsState 9 13 import androidx.compose.foundation.layout.* 10 14 import androidx.compose.foundation.rememberScrollState 11 - import androidx.compose.foundation.shape.RoundedCornerShape 12 15 import androidx.compose.foundation.verticalScroll 13 16 import androidx.compose.material.icons.Icons 14 17 import androidx.compose.material.icons.outlined.* ··· 17 20 import androidx.compose.ui.Alignment 18 21 import androidx.compose.ui.Modifier 19 22 import androidx.compose.ui.draw.alpha 23 + import androidx.compose.ui.draw.scale 20 24 import androidx.compose.ui.graphics.Color 21 25 import androidx.compose.ui.graphics.compositeOver 22 26 import androidx.compose.ui.graphics.vector.ImageVector ··· 32 36 import com.derakkuma.ui.icons.PersonPinCircleOutlined 33 37 import com.derakkuma.ui.icons.Signature 34 38 import com.derakkuma.ui.icons.SocialLeaderboardOutlined 39 + import com.derakkuma.ui.components.ExpressiveGlassCard 40 + import com.derakkuma.ui.components.ExpressiveTextButton 35 41 import com.derakkuma.ui.images.BundledIcon 36 42 import com.derakkuma.ui.images.BundledImage 37 43 import com.derakkuma.ui.theme.* 38 - import com.derakkuma.ui.theme.DangerRed 39 44 40 - private val SettingsCardTint = Color(0xFFBA47DE).asCardTint() 41 - private val CollectionCardTint = Color(0xFF3CB9ED).asCardTint() 42 - private val ShopCardTint = Color(0xFF5FDDB7).asCardTint() 43 - private val PlayDataCardTint = Color(0xFFEFB944).asCardTint() 44 - private val RankingCardTint = Color(0xFFBA6EEE).asCardTint() 45 - private val EventCardTint = Color(0xFF7CAB44).asCardTint() 46 - private val KamaitachiCardTint = Color(0xFFCD226F).asCardTint() 47 45 48 46 @Composable 49 47 fun MoreMenu( ··· 68 66 animationSpec = infiniteRepeatable(animation = tween(durationMillis = 8_000, easing = LinearEasing)), 69 67 label = "app-icon-hue", 70 68 ) 71 - val appIconRainbowTint = Color.hsv(appIconHue, 0.55f, 1f).asCardTint(alpha = 0.2f) 69 + val appIconRainbowTint = Color.hsv(appIconHue, 0.55f, 1f).asCardTint(base = MaterialTheme.colorScheme.surface, alpha = 0.2f) 70 + val settingsCardTint = MaterialTheme.colorScheme.primaryContainer 71 + val collectionCardTint = MaterialTheme.colorScheme.tertiaryContainer 72 + val shopCardTint = MaterialTheme.colorScheme.secondaryContainer 73 + val playDataCardTint = Color.White 74 + val rankingCardTint = MaterialTheme.colorScheme.primaryContainer 75 + val eventCardTint = MaterialTheme.colorScheme.tertiaryContainer 76 + val kamaitachiCardTint = MaterialTheme.colorScheme.errorContainer 77 + val favoriteSongsCardTint = ExpressiveBubblegum.copy(alpha = 0.18f).compositeOver(Color.White) 78 + val playerNameCardTint = ExpressivePeach.copy(alpha = 0.22f).compositeOver(Color.White) 79 + val mapCardTint = ExpressiveSky.copy(alpha = 0.18f).compositeOver(Color.White) 80 + val bubbleCardTint = ExpressiveMint.copy(alpha = 0.5f).compositeOver(Color.White) 81 + val appSettingsCardTint = ExpressiveGrape.copy(alpha = 0.16f).compositeOver(Color.White) 72 82 73 83 Column( 74 84 modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 88.dp), 75 85 verticalArrangement = Arrangement.spacedBy(8.dp), 76 86 ) { 77 - Text("More", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(bottom = 4.dp)) 87 + Text("More", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 4.dp)) 78 88 79 89 BentoCard( 80 90 icon = Icons.Outlined.Settings, ··· 83 93 description = "Note speed, timing, sound, display options, friend registration, etc.", 84 94 onClick = onSettings, 85 95 disabled = maintenanceMode, 86 - containerColor = SettingsCardTint, 96 + containerColor = settingsCardTint, 87 97 modifier = Modifier.fillMaxWidth(), 88 98 ) 89 99 ··· 95 105 description = "", 96 106 onClick = onCollection, 97 107 disabled = maintenanceMode, 98 - containerColor = CollectionCardTint, 108 + containerColor = collectionCardTint, 99 109 modifier = Modifier.weight(1f).fillMaxHeight(), 100 110 ) 101 111 BentoCard( ··· 107 117 onClick = {}, 108 118 disabled = true, 109 119 disabledLabel = "Coming soon", 110 - containerColor = ShopCardTint, 120 + containerColor = shopCardTint, 111 121 modifier = Modifier.weight(1f).fillMaxHeight(), 112 122 ) 113 123 } ··· 119 129 title = "Player Data", 120 130 onClick = onPlayerData, 121 131 compact = true, 122 - containerColor = PlayDataCardTint, 132 + containerColor = playDataCardTint, 123 133 modifier = Modifier.weight(1f).fillMaxHeight(), 124 134 ) 125 135 BentoCard( ··· 129 139 onClick = onFavorites, 130 140 disabled = maintenanceMode, 131 141 compact = true, 142 + containerColor = favoriteSongsCardTint, 132 143 modifier = Modifier.weight(1f).fillMaxHeight(), 133 144 ) 134 145 BentoCard( ··· 138 149 onClick = onPlayerName, 139 150 disabled = maintenanceMode, 140 151 compact = true, 152 + containerColor = playerNameCardTint, 141 153 modifier = Modifier.weight(1f).fillMaxHeight(), 142 154 ) 143 155 } ··· 149 161 title = "Leaderboards", 150 162 onClick = onRanking, 151 163 disabled = maintenanceMode, 152 - containerColor = RankingCardTint, 164 + containerColor = rankingCardTint, 153 165 modifier = Modifier.weight(1f).fillMaxHeight(), 154 166 ) 155 167 BentoCard( ··· 159 171 onClick = {}, 160 172 disabled = true, 161 173 disabledLabel = "Coming soon", 162 - containerColor = EventCardTint, 174 + containerColor = eventCardTint, 163 175 modifier = Modifier.weight(1f).fillMaxHeight(), 164 176 ) 165 177 } ··· 171 183 title = "Cab Map", 172 184 description = "", 173 185 onClick = onMap, 186 + containerColor = mapCardTint, 174 187 modifier = Modifier.weight(1f).fillMaxHeight(), 175 188 ) 176 189 BentoCard( ··· 179 192 title = "Bubble", 180 193 description = "", 181 194 onClick = onBubble, 195 + containerColor = bubbleCardTint, 182 196 modifier = Modifier.weight(1f).fillMaxHeight(), 183 197 ) 184 198 } ··· 190 204 title = "App Settings", 191 205 description = "Cache, account, about", 192 206 onClick = onAppSettings, 207 + containerColor = appSettingsCardTint, 193 208 modifier = Modifier.weight(1f).fillMaxHeight(), 194 209 ) 195 210 BentoCard( ··· 199 214 description = "Sync records", 200 215 onClick = onKamaitachi, 201 216 disabled = maintenanceMode, 202 - containerColor = KamaitachiCardTint, 217 + containerColor = kamaitachiCardTint, 203 218 modifier = Modifier.weight(1f).fillMaxHeight(), 204 219 ) 205 220 } ··· 220 235 } 221 236 222 237 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 223 - TextButton(onClick = { openOAuthUrl("https://derakkuma.com/terms") }) { 238 + ExpressiveTextButton(onClick = { openOAuthUrl("https://derakkuma.com/terms") }) { 224 239 Text("Terms of Service", fontSize = 11.sp, color = Color.White) 225 240 } 226 - TextButton(onClick = { openOAuthUrl("https://derakkuma.com/privacy") }) { 241 + ExpressiveTextButton(onClick = { openOAuthUrl("https://derakkuma.com/privacy") }) { 227 242 Text("Privacy Policy", fontSize = 11.sp, color = Color.White) 228 243 } 229 244 } 230 245 } 231 246 } 232 247 233 - private fun Color.asCardTint(alpha: Float = 0.14f): Color = copy(alpha = alpha).compositeOver(Color.White) 248 + private fun Color.asCardTint(base: Color, alpha: Float = 0.14f): Color = copy(alpha = alpha).compositeOver(base) 234 249 235 250 @Composable 236 251 private fun BentoRow(content: @Composable RowScope.() -> Unit) { ··· 254 269 descriptionIcon: BundledImage? = null, 255 270 onClick: () -> Unit, 256 271 modifier: Modifier = Modifier, 257 - containerColor: Color = CardWhite, 272 + containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, 258 273 disabled: Boolean = false, 259 274 disabledLabel: String = "Maintenance", 260 275 compact: Boolean = false, 261 276 ) { 262 277 val hasDetails = description.isNotBlank() || disabled 278 + val interactionSource = remember { MutableInteractionSource() } 279 + val pressed by interactionSource.collectIsPressedAsState() 280 + val scale by animateFloatAsState( 281 + targetValue = if (pressed && !disabled) 0.97f else 1f, 282 + animationSpec = spring(dampingRatio = 0.62f, stiffness = 460f), 283 + label = "more-card-press-scale", 284 + ) 263 285 264 - Card( 265 - modifier = modifier, 266 - shape = RoundedCornerShape(8.dp), 267 - colors = CardDefaults.cardColors(containerColor = containerColor), 286 + ExpressiveGlassCard( 287 + modifier = modifier.scale(scale), 288 + cornerRadius = if (compact) 22.dp else 28.dp, 289 + containerColor = containerColor, 268 290 ) { 269 - Box(modifier = Modifier.fillMaxSize().clickable(enabled = !disabled, onClick = onClick)) { 291 + Box( 292 + modifier = Modifier 293 + .fillMaxSize() 294 + .clickable(enabled = !disabled, interactionSource = interactionSource, indication = null, onClick = onClick), 295 + ) { 270 296 backgroundStartImage?.let { image -> 271 297 BundledIcon( 272 298 image = image, ··· 297 323 modifier = Modifier.size(26.dp).alpha(if (disabled) 0.45f else 1f), 298 324 ) 299 325 } else { 300 - Icon(icon, contentDescription = null, modifier = Modifier.size(26.dp), tint = if (disabled) MutedText.copy(alpha = 0.4f) else PrimaryBlue) 326 + Icon(icon, contentDescription = null, modifier = Modifier.size(26.dp), tint = if (disabled) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) else MaterialTheme.colorScheme.primary) 301 327 } 302 328 } 303 - Text(title, fontSize = if (compact) 13.sp else 15.sp, fontWeight = FontWeight.SemiBold, color = if (disabled) MutedText else DarkText) 329 + Text(title, fontSize = if (compact) 13.sp else 15.sp, fontWeight = FontWeight.SemiBold, color = if (disabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface) 304 330 } 305 331 if (hasDetails) { 306 332 Column( ··· 323 349 Text( 324 350 description, 325 351 fontSize = 11.sp, 326 - color = MutedText, 352 + color = MaterialTheme.colorScheme.onSurfaceVariant, 327 353 lineHeight = 13.sp, 328 354 textAlign = if (centerText) TextAlign.Center else TextAlign.Start, 329 355 ) 330 356 } 331 357 } 332 358 if (disabled) { 333 - Text(disabledLabel, fontSize = 10.sp, fontWeight = FontWeight.Bold, color = DangerRed.copy(alpha = 0.7f)) 359 + Text(disabledLabel, fontSize = 10.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.error.copy(alpha = 0.7f)) 334 360 } 335 361 } 336 362 }
+29 -39
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/MusicDetailTab.kt
··· 2 2 3 3 import androidx.compose.foundation.layout.* 4 4 import androidx.compose.foundation.rememberScrollState 5 - import androidx.compose.foundation.shape.RoundedCornerShape 6 5 import androidx.compose.foundation.verticalScroll 6 + import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 7 + import androidx.compose.material3.MaterialShapes 7 8 import androidx.compose.material3.* 9 + import androidx.compose.material3.toShape 8 10 import androidx.compose.runtime.* 9 11 import androidx.compose.ui.Alignment 10 12 import androidx.compose.ui.Modifier ··· 22 24 import com.derakkuma.data.CacheStore 23 25 import com.derakkuma.ui.components.EmptyState 24 26 import com.derakkuma.ui.components.ErrorState 27 + import com.derakkuma.ui.components.ExpressiveGlassCard 28 + import com.derakkuma.ui.components.ExpressiveHorizontalDivider 29 + import com.derakkuma.ui.components.ExpressiveBadgeSurface 25 30 import com.derakkuma.ui.components.LoadingState 26 31 import com.derakkuma.ui.images.* 27 32 import com.derakkuma.ui.theme.* 28 33 import com.derakkuma.util.formatScore 29 34 35 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 30 36 @Composable 31 37 fun MusicDetailTab(client: MaimaiClient, cookie: String, idx: String, maintenanceMode: Boolean = false, cacheStore: CacheStore = CacheStore(), atProtoClient: AtProtoClient? = null) { 32 38 var detail by remember { mutableStateOf<MusicDetail?>(null) } ··· 77 83 val d = detail!! 78 84 79 85 // Song header 80 - Card( 81 - modifier = Modifier.fillMaxWidth(), 82 - shape = RoundedCornerShape(8.dp), 83 - colors = CardDefaults.cardColors(containerColor = CardWhite), 84 - ) { 86 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 85 87 Row( 86 88 modifier = Modifier.padding(12.dp), 87 89 verticalAlignment = Alignment.CenterVertically, ··· 90 92 if (d.albumArtUrl != null) { 91 93 MaimaiRemoteImage( 92 94 url = d.albumArtUrl, 93 - modifier = Modifier.size(60.dp).clip(RoundedCornerShape(4.dp)), 95 + modifier = Modifier.size(60.dp).clip(MaterialShapes.Square.toShape()), 94 96 contentDescription = d.name, 95 97 contentScale = ContentScale.Crop, 96 98 ) 97 99 } 98 100 Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { 99 - Text(d.name, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText, maxLines = 2) 101 + Text(d.name, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, maxLines = 2) 100 102 if (d.artist.isNotBlank()) { 101 - Text(d.artist, fontSize = 12.sp, color = MutedText) 103 + Text(d.artist, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 102 104 } 103 105 } 104 106 } ··· 108 110 109 111 // All difficulty scores 110 112 if (d.scores.isNotEmpty()) { 111 - Card( 112 - modifier = Modifier.fillMaxWidth(), 113 - shape = RoundedCornerShape(8.dp), 114 - colors = CardDefaults.cardColors(containerColor = CardWhite), 115 - ) { 113 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 116 114 Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { 117 - Text("All Difficulties", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 115 + Text("All Difficulties", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 118 116 119 117 d.scores.forEach { score -> 120 118 DifficultyScoreRow(score) 121 119 if (score != d.scores.last()) { 122 - HorizontalDivider(color = MutedText.copy(alpha = 0.15f)) 120 + ExpressiveHorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f)) 123 121 } 124 122 } 125 123 } 126 124 } 127 125 } else { 128 - Card( 129 - modifier = Modifier.fillMaxWidth(), 130 - shape = RoundedCornerShape(8.dp), 131 - colors = CardDefaults.cardColors(containerColor = CardWhite), 132 - ) { 126 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 24.dp) { 133 127 Box(Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) { 134 - Text("No scores recorded", fontSize = 14.sp, color = MutedText) 128 + Text("No scores recorded", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 135 129 } 136 130 } 137 131 } ··· 147 141 leaderboards: Map<String, List<AtProtoLeaderboardEntry>>, 148 142 error: String?, 149 143 ) { 150 - Card( 151 - modifier = Modifier.fillMaxWidth(), 152 - shape = RoundedCornerShape(8.dp), 153 - colors = CardDefaults.cardColors(containerColor = CardWhite), 154 - ) { 144 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 155 145 Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 156 - Text("AT Protocol Leaderboards", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 146 + Text("AT Protocol Leaderboards", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 157 147 if (error != null) { 158 148 Text(error, fontSize = 12.sp, color = MaterialTheme.colorScheme.error) 159 149 return@Column 160 150 } 161 151 if (leaderboards.isEmpty()) { 162 - Text("No published scores yet.", fontSize = 12.sp, color = MutedText) 152 + Text("No published scores yet.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 163 153 } else { 164 154 leaderboards.forEach { (difficulty, entries) -> 165 155 Text(difficulty.toAtProtoDifficultyName(), fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = difficultyColor(difficulty)) 166 156 if (entries.isEmpty()) { 167 - Text("No entries", fontSize = 11.sp, color = MutedText) 157 + Text("No entries", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 168 158 } else { 169 159 entries.take(5).forEachIndexed { index, entry -> 170 160 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { 171 161 val label = entry.playerName.ifBlank { entry.did.take(12) + "…" } 172 - Text("#${entry.rank.takeIf { it > 0 } ?: index + 1} $label", fontSize = 12.sp, color = DarkText, modifier = Modifier.weight(1f)) 173 - Text("${formatScore(entry.achievement.toDoubleOrNull() ?: 0.0)}%", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = AccentPink) 162 + Text("#${entry.rank.takeIf { it > 0 } ?: index + 1} $label", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f)) 163 + Text("${formatScore(entry.achievement.toDoubleOrNull() ?: 0.0)}%", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.secondary) 174 164 } 175 165 } 176 166 } ··· 187 177 verticalAlignment = Alignment.CenterVertically, 188 178 ) { 189 179 // Difficulty badge 190 - Surface( 191 - shape = RoundedCornerShape(4.dp), 192 - color = difficultyColor(score.difficulty).copy(alpha = 0.15f), 180 + ExpressiveBadgeSurface( 181 + shape = MaterialTheme.shapes.large, 182 + containerColor = difficultyColor(score.difficulty).copy(alpha = 0.15f), 193 183 ) { 194 184 Text( 195 185 " ${score.difficulty.replaceFirstChar { it.uppercase() }} ${score.level} ", ··· 214 204 "${formatScore(score.achievement)}%", 215 205 fontSize = 13.sp, 216 206 fontWeight = FontWeight.Bold, 217 - color = AccentPink, 207 + color = MaterialTheme.colorScheme.secondary, 218 208 ) 219 209 } 220 210 if (score.scoreRank.isNotBlank()) { ··· 237 227 // DX score + play info 238 228 Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { 239 229 if (score.dxScore.achieved > 0) { 240 - Text("DX ${score.dxScore.achieved}/${score.dxScore.total}", fontSize = 10.sp, color = MutedText) 230 + Text("DX ${score.dxScore.achieved}/${score.dxScore.total}", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 241 231 } 242 232 if (score.playCount > 0) { 243 - Text("Plays: ${score.playCount}", fontSize = 10.sp, color = MutedText) 233 + Text("Plays: ${score.playCount}", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 244 234 } 245 235 if (score.lastPlayed.isNotBlank()) { 246 - Text(score.lastPlayed, fontSize = 10.sp, color = MutedText) 236 + Text(score.lastPlayed, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 247 237 } 248 238 } 249 239 }
+42 -53
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/PlayDetailTab.kt
··· 2 2 3 3 import androidx.compose.foundation.layout.* 4 4 import androidx.compose.foundation.rememberScrollState 5 - import androidx.compose.foundation.shape.RoundedCornerShape 6 5 import androidx.compose.foundation.verticalScroll 6 + import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 7 + import androidx.compose.material3.MaterialShapes 7 8 import androidx.compose.material3.* 9 + import androidx.compose.material3.toShape 8 10 import androidx.compose.runtime.* 9 11 import androidx.compose.ui.Alignment 10 12 import androidx.compose.ui.Modifier ··· 20 22 import com.derakkuma.data.CacheStore 21 23 import com.derakkuma.ui.components.EmptyState 22 24 import com.derakkuma.ui.components.ErrorState 25 + import com.derakkuma.ui.components.ExpressiveGlassCard 26 + import com.derakkuma.ui.components.ExpressiveHorizontalDivider 23 27 import com.derakkuma.ui.components.LoadingState 24 28 import com.derakkuma.ui.images.* 25 29 import com.derakkuma.ui.theme.* ··· 34 38 import network.chaintech.cmpcharts.ui.segmentprogresschart.model.SegmentProgressBarColors 35 39 import network.chaintech.cmpcharts.ui.segmentprogresschart.model.SegmentProgressBarConfig 36 40 41 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 37 42 @Composable 38 43 fun PlayDetailTab(client: MaimaiClient, cookie: String, idx: String, onMusicDetail: ((idx: String) -> Unit)? = null, maintenanceMode: Boolean = false, cacheStore: CacheStore = CacheStore(), playStore: PlayStore? = null) { 39 44 var detail by remember { mutableStateOf<PlayLogDetail?>(null) } ··· 73 78 val d = detail!! 74 79 75 80 // Song header 76 - Card( 77 - modifier = Modifier.fillMaxWidth(), 78 - shape = RoundedCornerShape(8.dp), 79 - colors = CardDefaults.cardColors(containerColor = CardWhite), 80 - ) { 81 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 81 82 Row( 82 83 modifier = Modifier.padding(12.dp), 83 84 verticalAlignment = Alignment.CenterVertically, ··· 86 87 if (d.albumArtUrl != null) { 87 88 MaimaiRemoteImage( 88 89 url = d.albumArtUrl, 89 - modifier = Modifier.size(60.dp).clip(RoundedCornerShape(4.dp)), 90 + modifier = Modifier.size(60.dp).clip(MaterialShapes.Square.toShape()), 90 91 contentDescription = d.name, 91 92 contentScale = ContentScale.Crop, 92 93 ) 93 94 } 94 95 Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { 95 - Text(d.name, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText, maxLines = 2) 96 + Text(d.name, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, maxLines = 2) 96 97 Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) { 97 98 val levelText = songDbLevel?.let { "Lv ${d.level} (${formatScore(it, 1)})" } ?: "Lv ${d.level}" 98 99 Text(levelText, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = difficultyColor(d.difficulty)) 99 100 if (d.isDx) BundledIcon(image = BundledImage.MusicDx, modifier = Modifier.height(12.dp), contentDescription = "DX") 100 101 } 101 - Text(d.datetime, fontSize = 11.sp, color = MutedText) 102 + Text(d.datetime, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 102 103 } 103 104 } 104 105 } ··· 106 107 Spacer(Modifier.height(8.dp)) 107 108 108 109 // Score header 109 - Card( 110 - modifier = Modifier.fillMaxWidth(), 111 - shape = RoundedCornerShape(8.dp), 112 - colors = CardDefaults.cardColors(containerColor = CardWhite), 113 - ) { 110 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 114 111 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 115 112 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { 116 113 BigStat("${formatScore(d.achievement)}%", "Achievement") ··· 123 120 val deltaColor = if (d.ratingDelta > 0) { 124 121 DiffBasic 125 122 } else if (d.ratingDelta < 0) { 126 - DangerRed 123 + MaterialTheme.colorScheme.error 127 124 } else { 128 - MutedText 125 + MaterialTheme.colorScheme.onSurfaceVariant 129 126 } 130 127 val sign = if (d.ratingDelta > 0) "+" else "" 131 128 BigStat("$sign${d.ratingDelta}", "Delta", color = deltaColor) ··· 140 137 BigStat("${d.maxSync.achieved}/${d.maxSync.total}", "Max Sync") 141 138 Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { 142 139 FastLateStat("Fast", d.fast, DiffBasic) 143 - FastLateStat("Late", d.late, DangerRed) 140 + FastLateStat("Late", d.late, MaterialTheme.colorScheme.error) 144 141 } 145 142 } 146 143 } ··· 153 150 Spacer(Modifier.height(8.dp)) 154 151 155 152 // Note breakdown table 156 - Card( 157 - modifier = Modifier.fillMaxWidth(), 158 - shape = RoundedCornerShape(8.dp), 159 - colors = CardDefaults.cardColors(containerColor = CardWhite), 160 - ) { 153 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 161 154 Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { 162 - Text("Note Breakdown", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 155 + Text("Note Breakdown", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 163 156 NoteBreakdownTable(d.notes) 164 157 } 165 158 } ··· 171 164 // Button( 172 165 // onClick = { onMusicDetail(d.musicIdx) }, 173 166 // modifier = Modifier.fillMaxWidth(), 174 - // shape = RoundedCornerShape(8.dp), 175 - // colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), 167 + // shape = MaterialTheme.shapes.medium, 168 + // colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 176 169 // ) { 177 170 // Text("View All Difficulties", fontSize = 14.sp, fontWeight = FontWeight.SemiBold) 178 171 // } ··· 188 181 if (rankIcon != null) { 189 182 BundledIcon(image = rankIcon, modifier = Modifier.height(28.dp), contentDescription = rank) 190 183 } else { 191 - Text(rank.uppercase(), fontSize = 18.sp, fontWeight = FontWeight.Bold, color = DarkText) 184 + Text(rank.uppercase(), fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 192 185 } 193 - Text("Rank", fontSize = 10.sp, color = MutedText) 186 + Text("Rank", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 194 187 } 195 188 } 196 189 197 190 @Composable 198 - private fun BigStat(value: String, label: String, color: androidx.compose.ui.graphics.Color = DarkText) { 191 + private fun BigStat(value: String, label: String, color: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface) { 199 192 Column(horizontalAlignment = Alignment.CenterHorizontally) { 200 193 Text(value, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = color) 201 - Text(label, fontSize = 10.sp, color = MutedText) 194 + Text(label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 202 195 } 203 196 } 204 197 ··· 206 199 private fun FastLateStat(label: String, count: Int, color: androidx.compose.ui.graphics.Color) { 207 200 Column(horizontalAlignment = Alignment.CenterHorizontally) { 208 201 Text("$count", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = color) 209 - Text(label, fontSize = 10.sp, color = MutedText) 202 + Text(label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 210 203 } 211 204 } 212 205 ··· 226 219 modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), 227 220 horizontalArrangement = Arrangement.SpaceBetween, 228 221 ) { 229 - Text("Type", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = MutedText, modifier = Modifier.weight(1.2f)) 222 + Text("Type", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1.2f)) 230 223 columns.drop(1).forEach { col -> 231 - Text(col, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = MutedText, modifier = Modifier.weight(1f), textAlign = TextAlign.Center) 224 + Text(col, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f), textAlign = TextAlign.Center) 232 225 } 233 226 } 234 227 235 - HorizontalDivider(color = MutedText.copy(alpha = 0.15f)) 228 + ExpressiveHorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f)) 236 229 237 230 rows.forEach { (label, judgement) -> 238 231 Row( 239 232 modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp), 240 233 horizontalArrangement = Arrangement.SpaceBetween, 241 234 ) { 242 - Text(label, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = DarkText, modifier = Modifier.weight(1.2f)) 235 + Text(label, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1.2f)) 243 236 val values = listOf(judgement.criticalPerfect, judgement.perfect, judgement.great, judgement.good, judgement.miss) 244 237 values.forEach { v -> 245 238 Text( 246 239 if (v > 0) "$v" else "-", 247 240 fontSize = 12.sp, 248 - color = if (v > 0) DarkText else MutedText.copy(alpha = 0.4f), 241 + color = if (v > 0) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), 249 242 modifier = Modifier.weight(1f), 250 243 textAlign = TextAlign.Center, 251 244 ) ··· 257 250 @Composable 258 251 private fun PlayBreakdownCharts(detail: PlayLogDetail) { 259 252 val radarData = detail.notes.noteTypeScores() 260 - Card( 261 - modifier = Modifier.fillMaxWidth(), 262 - shape = RoundedCornerShape(8.dp), 263 - colors = CardDefaults.cardColors(containerColor = CardWhite), 264 - ) { 253 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 265 254 Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { 266 - Text("Performance Shape", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 255 + Text("Performance Shape", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 267 256 if (radarData.any { it.second > 0.0 }) { 268 257 RadarChart( 269 258 modifier = Modifier.fillMaxWidth().height(230.dp), ··· 272 261 scaleStepsCount = 4, 273 262 maxLabelWidth = 40.dp, 274 263 radarChartLineStyle = RadarChartLineStyle( 275 - color = MutedText.copy(alpha = 0.25f), 264 + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f), 276 265 strokeWidth = 1f, 277 266 strokeCap = StrokeCap.Butt, 278 267 ), 279 268 radarChartDataSet = listOf( 280 269 RadarChartDataSet( 281 270 style = RadarChartDataSetStyle( 282 - color = PrimaryBlue, 283 - borderColor = AccentPink, 271 + color = MaterialTheme.colorScheme.primary, 272 + borderColor = MaterialTheme.colorScheme.secondary, 284 273 colorAlpha = 0.22f, 285 274 strokeWidth = 2f, 286 275 strokeCap = StrokeCap.Round, ··· 288 277 data = radarData.map { it.second }, 289 278 ), 290 279 ), 291 - labelTextStyle = TextStyle(color = DarkText, fontSize = 11.sp, fontWeight = FontWeight.SemiBold), 280 + labelTextStyle = TextStyle(color = MaterialTheme.colorScheme.onSurface, fontSize = 11.sp, fontWeight = FontWeight.SemiBold), 292 281 scaleTextStyle = TextStyle(color = androidx.compose.ui.graphics.Color.Transparent, fontSize = 1.sp), 293 282 scaleUnit = "%", 294 283 maxScaleValue = 100.0, ··· 296 285 ), 297 286 ) 298 287 } else { 299 - Text("No judgement data for this play", fontSize = 12.sp, color = MutedText) 288 + Text("No judgement data for this play", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 300 289 } 301 290 302 291 val timingTotal = detail.fast + detail.late 303 292 if (timingTotal > 0) { 304 - Text("Timing Bias", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 305 - Text("Late ${detail.late} • Fast ${detail.fast}", fontSize = 12.sp, color = MutedText) 293 + Text("Timing Bias", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 294 + Text("Late ${detail.late} • Fast ${detail.fast}", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 306 295 SegmentedProgressBarChart( 307 296 modifier = Modifier.fillMaxWidth(), 308 297 segmentProgressBarConfig = SegmentProgressBarConfig( ··· 313 302 progress = (detail.fast.toFloat() / timingTotal.toFloat()).coerceIn(0f, 1f), 314 303 scaleLabels = listOf("Late", "", "Even", "", "Fast"), 315 304 segmentsList = listOf( 316 - SegmentInfo(DangerRed, 1f), 305 + SegmentInfo(MaterialTheme.colorScheme.error, 1f), 317 306 SegmentInfo(DiffAdvanced, 1f), 318 307 SegmentInfo(DiffBasic.copy(alpha = 0.55f), 1f), 319 308 SegmentInfo(DiffAdvanced, 1f), 320 - SegmentInfo(DangerRed, 1f), 309 + SegmentInfo(MaterialTheme.colorScheme.error, 1f), 321 310 ), 322 311 displayScale = true, 323 312 scaleFontSize = 9.sp, 324 313 shouldAnimateProgress = true, 325 314 segmentProgressBarColors = SegmentProgressBarColors( 326 - pointerColor = DarkText, 327 - scaleTextColor = MutedText, 328 - scaleColor = MutedText.copy(alpha = 0.35f), 315 + pointerColor = MaterialTheme.colorScheme.onSurface, 316 + scaleTextColor = MaterialTheme.colorScheme.onSurfaceVariant, 317 + scaleColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f), 329 318 ), 330 319 ), 331 320 )
+129 -119
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/PlayerDataTab.kt
··· 1 1 package com.derakkuma.ui.tabs 2 2 3 + import com.derakkuma.ui.components.ExpressiveLinearProgressIndicator 3 4 import androidx.compose.animation.core.animateFloatAsState 4 5 import androidx.compose.animation.core.tween 5 6 import androidx.compose.foundation.Canvas 6 7 import androidx.compose.foundation.Image 8 + import androidx.compose.foundation.background 7 9 import androidx.compose.foundation.layout.* 8 10 import androidx.compose.foundation.rememberScrollState 9 - import androidx.compose.foundation.shape.CircleShape 10 11 import androidx.compose.foundation.shape.RoundedCornerShape 11 12 import androidx.compose.foundation.verticalScroll 12 13 import androidx.compose.material3.* ··· 28 29 import com.derakkuma.data.CacheStore 29 30 import com.derakkuma.ui.components.BigStat 30 31 import com.derakkuma.ui.components.ErrorState 32 + import com.derakkuma.ui.components.ExpressiveGlassCard 33 + import com.derakkuma.ui.components.ExpressiveDot 31 34 import com.derakkuma.ui.components.LoadingState 32 35 import com.derakkuma.ui.images.* 33 36 import com.derakkuma.ui.theme.* ··· 81 84 82 85 // Mini profile header 83 86 if (profile != null) { 84 - Card( 85 - modifier = Modifier.fillMaxWidth(), 86 - shape = RoundedCornerShape(8.dp), 87 - colors = CardDefaults.cardColors(containerColor = CardWhite), 88 - ) { 87 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 24.dp) { 89 88 Row( 90 89 modifier = Modifier.padding(12.dp), 91 90 verticalAlignment = Alignment.CenterVertically, ··· 93 92 ) { 94 93 MaimaiRemoteImage( 95 94 url = profile.profileImageUrl, 96 - modifier = Modifier.size(44.dp).clip(CircleShape), 95 + modifier = Modifier.size(44.dp).clip(RoundedCornerShape(2.dp)), 97 96 contentDescription = "Profile icon", 98 97 contentScale = ContentScale.Crop, 99 98 ) 100 99 Column(modifier = Modifier.weight(1f)) { 101 - Text(profile.name, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 102 - Text("Rating: ${profile.rating}", fontSize = 13.sp, color = MutedText) 100 + Text(profile.name, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 101 + Text("Rating: ${profile.rating}", fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 103 102 } 104 103 } 105 104 } ··· 108 107 } 109 108 110 109 // Play count stats 111 - Card( 112 - modifier = Modifier.fillMaxWidth(), 113 - shape = RoundedCornerShape(8.dp), 114 - colors = CardDefaults.cardColors(containerColor = CardWhite), 115 - ) { 110 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 116 111 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { 117 - Text("Play Statistics", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 112 + Text("Play Statistics", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 118 113 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { 119 114 BigStat("${pd.playCount.currentVersion}", "Current Ver") 120 115 BigStat("${pd.playCount.total}", "Dashboard") ··· 126 121 Spacer(Modifier.height(8.dp)) 127 122 128 123 // Maimile 129 - Card( 130 - modifier = Modifier.fillMaxWidth(), 131 - shape = RoundedCornerShape(8.dp), 132 - colors = CardDefaults.cardColors(containerColor = CardWhite), 133 - ) { 124 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 24.dp) { 134 125 Row( 135 126 modifier = Modifier.padding(16.dp).fillMaxWidth(), 136 127 verticalAlignment = Alignment.CenterVertically, ··· 145 136 modifier = Modifier.height(18.dp), 146 137 contentDescription = "maimile", 147 138 ) 148 - Text("maimile", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 139 + Text("maimile", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 149 140 } 150 - Text("${pd.maimile}", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = AccentPink) 141 + Text("${pd.maimile}", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.secondary) 151 142 } 152 143 } 153 144 ··· 163 154 164 155 // Class CP 165 156 if (pd.classCP.max > 0) { 166 - Card( 167 - modifier = Modifier.fillMaxWidth(), 168 - shape = RoundedCornerShape(8.dp), 169 - colors = CardDefaults.cardColors(containerColor = CardWhite), 170 - ) { 157 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 171 158 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 172 - Text("Class CP", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 159 + Text("Class CP", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 173 160 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { 174 161 BigStat("${pd.classCP.current}", "Current") 175 162 BigStat("${pd.classCP.max}", "Max") ··· 181 168 animationSpec = tween(durationMillis = 450), 182 169 label = "class-cp-progress", 183 170 ) 184 - LinearProgressIndicator( 171 + ExpressiveLinearProgressIndicator( 185 172 progress = { progress }, 186 - modifier = Modifier.fillMaxWidth().height(8.dp).clip(RoundedCornerShape(4.dp)), 187 - color = PrimaryBlue, 188 - trackColor = MutedText.copy(alpha = 0.15f), 173 + modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge), 174 + color = MaterialTheme.colorScheme.primary, 175 + trackColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f), 189 176 ) 190 177 } 191 178 } ··· 196 183 197 184 // Score rank counters 198 185 if (pd.scoreCounters.isNotEmpty()) { 199 - Card( 200 - modifier = Modifier.fillMaxWidth(), 201 - shape = RoundedCornerShape(8.dp), 202 - colors = CardDefaults.cardColors(containerColor = CardWhite), 203 - ) { 186 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 204 187 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 205 - Text("Score Counters", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 206 - pd.scoreCounters.forEach { counter -> 207 - Row( 208 - modifier = Modifier.fillMaxWidth(), 209 - horizontalArrangement = Arrangement.SpaceBetween, 210 - ) { 211 - Text(counter.rank.uppercase(), fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 212 - Text("${counter.achieved} / ${counter.total}", fontSize = 14.sp, color = MutedText) 213 - } 214 - if (counter.total > 0) { 215 - val progress by animateFloatAsState( 216 - targetValue = (counter.achieved.toFloat() / counter.total.toFloat()).coerceIn(0f, 1f), 217 - animationSpec = tween(durationMillis = 450), 218 - label = "score-counter-progress", 219 - ) 220 - LinearProgressIndicator( 221 - progress = { progress }, 222 - modifier = Modifier.fillMaxWidth().height(4.dp).clip(RoundedCornerShape(2.dp)), 223 - color = PrimaryBlue, 224 - trackColor = MutedText.copy(alpha = 0.15f), 225 - ) 226 - } 227 - } 188 + Text("Score Counters", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 189 + ScoreCounterBarChart(pd.scoreCounters, modifier = Modifier.fillMaxWidth().height(230.dp)) 228 190 } 229 191 } 230 192 ··· 233 195 234 196 // Missions 235 197 if (pd.missions.tasks.isNotEmpty() || pd.missions.deadline.isNotEmpty()) { 236 - Card( 237 - modifier = Modifier.fillMaxWidth(), 238 - shape = RoundedCornerShape(8.dp), 239 - colors = CardDefaults.cardColors(containerColor = CardWhite), 240 - ) { 198 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 241 199 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 242 - Text("Missions", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 200 + Text("Missions", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 243 201 if (pd.missions.deadline.isNotBlank()) { 244 - Text("Until ${pd.missions.deadline}", fontSize = 12.sp, color = MutedText) 202 + Text("Until ${pd.missions.deadline}", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 245 203 } 246 - Text("Progress: ${pd.missions.progress}", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue) 204 + Text("Progress: ${pd.missions.progress}", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) 247 205 pd.missions.tasks.forEach { task -> 248 206 Row( 249 207 modifier = Modifier.fillMaxWidth(), 250 208 horizontalArrangement = Arrangement.SpaceBetween, 251 209 ) { 252 - Text(task.description, fontSize = 13.sp, color = DarkText, modifier = Modifier.weight(1f)) 253 - Text("+${task.reward}", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = AccentPink) 210 + Text(task.description, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f)) 211 + Text("+${task.reward}", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.secondary) 254 212 } 255 213 } 256 214 } ··· 272 230 } 273 231 274 232 @Composable 233 + private fun ScoreCounterBarChart(counters: List<ScoreCounter>, modifier: Modifier = Modifier) { 234 + val displayCounters = remember(counters) { counters.filter { it.total > 0 || it.achieved > 0 } } 235 + if (displayCounters.isEmpty()) return 236 + val animatedFractions = displayCounters.mapIndexed { index, counter -> 237 + val target = if (counter.total > 0) counter.achieved.toFloat() / counter.total.toFloat() else 0f 238 + animateFloatAsState( 239 + targetValue = target.coerceIn(0f, 1f), 240 + animationSpec = tween(durationMillis = 520 + index * 55), 241 + label = "score-counter-progress-${counter.rank}", 242 + ).value 243 + } 244 + 245 + Column( 246 + modifier = modifier, 247 + verticalArrangement = Arrangement.spacedBy(10.dp), 248 + ) { 249 + displayCounters.forEachIndexed { index, counter -> 250 + val color = scoreCounterColor(index, MaterialTheme.colorScheme.primary) 251 + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 252 + Row( 253 + modifier = Modifier.fillMaxWidth(), 254 + horizontalArrangement = Arrangement.SpaceBetween, 255 + verticalAlignment = Alignment.CenterVertically, 256 + ) { 257 + Text(counter.rank.uppercase(), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = color) 258 + Text("${counter.achieved} / ${counter.total}", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 259 + } 260 + Box( 261 + modifier = Modifier 262 + .fillMaxWidth() 263 + .height(12.dp) 264 + .clip(MaterialTheme.shapes.extraLarge) 265 + .background(color.copy(alpha = 0.14f)), 266 + ) { 267 + Box( 268 + modifier = Modifier 269 + .fillMaxHeight() 270 + .fillMaxWidth(animatedFractions[index]) 271 + .clip(MaterialTheme.shapes.extraLarge) 272 + .background(color), 273 + ) 274 + } 275 + } 276 + } 277 + } 278 + } 279 + 280 + private fun scoreCounterColor(index: Int, primaryColor: Color): Color { 281 + val colors = listOf(primaryColor, DiffMaster, DiffExpert, DiffAdvanced, DiffBasic) 282 + return colors[index % colors.size] 283 + } 284 + 285 + @Composable 275 286 private fun StatusChip(label: String, has: Boolean) { 276 - Card( 277 - shape = RoundedCornerShape(8.dp), 278 - colors = CardDefaults.cardColors( 279 - containerColor = if (has) PrimaryBlue.copy(alpha = 0.1f) else MutedText.copy(alpha = 0.08f), 280 - ), 287 + ExpressiveGlassCard( 288 + cornerRadius = 22.dp, 289 + containerColor = if (has) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHigh, 281 290 ) { 282 291 Row( 283 292 modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), 284 293 horizontalArrangement = Arrangement.spacedBy(6.dp), 285 294 verticalAlignment = Alignment.CenterVertically, 286 295 ) { 287 - Text(label, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = if (has) PrimaryBlue else MutedText) 296 + Text(label, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = if (has) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant) 288 297 if (has) { 289 - Text("!", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = AccentPink) 298 + Text("!", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.secondary) 290 299 } 291 300 } 292 301 } ··· 299 308 val trend = remember(validPlays) { validPlays.weeklyAverageTrend().takeLast(24) } 300 309 val levelTrend = remember(validPlays, songDbReady) { validPlays.weeklyLevelTrend(songDb).takeLast(24) } 301 310 302 - Card( 303 - modifier = Modifier.fillMaxWidth(), 304 - shape = RoundedCornerShape(8.dp), 305 - colors = CardDefaults.cardColors(containerColor = CardWhite), 306 - ) { 311 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 307 312 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { 308 - Text("Archived Play Averages", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 313 + Text("Archived Play Averages", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 309 314 if (validPlays.isEmpty()) { 310 - Text("No archived score data yet", fontSize = 12.sp, color = MutedText) 315 + Text("No archived score data yet", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 311 316 return@Column 312 317 } 313 318 ··· 317 322 } 318 323 319 324 if (trend.size >= 2) { 320 - Text("Average Over Time", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 325 + Text("Average Over Time", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 321 326 TransparentLineChart(trend, modifier = Modifier.fillMaxWidth().height(165.dp)) 322 327 } 323 328 324 329 if (levelTrend.size >= 2) { 325 - Text("Average Difficulty Over Time", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 330 + Text("Average Difficulty Over Time", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 326 331 TransparentLineChart(levelTrend, modifier = Modifier.fillMaxWidth().height(165.dp), unit = "") 327 332 } 328 333 } ··· 331 336 332 337 @Composable 333 338 private fun ScoreRankDistributionCard(plays: List<PlayStore.HistoricalPlay>) { 334 - val rankCounts = remember(plays) { plays.rankCounts() } 335 - val modeCounts = remember(plays) { plays.modeCounts() } 339 + val primaryColor = MaterialTheme.colorScheme.primary 340 + val rankCounts = remember(plays, primaryColor) { plays.rankCounts(primaryColor) } 341 + val modeCounts = remember(plays, primaryColor) { plays.modeCounts(primaryColor) } 336 342 val diffAverages = remember(plays) { plays.filter { it.achievement > 0.0 }.difficultyAverages() } 337 343 if (rankCounts.isEmpty() && modeCounts.isEmpty() && diffAverages.isEmpty()) return 338 344 339 - Card( 340 - modifier = Modifier.fillMaxWidth(), 341 - shape = RoundedCornerShape(8.dp), 342 - colors = CardDefaults.cardColors(containerColor = CardWhite), 343 - ) { 345 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 344 346 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { 345 - Text("Play Mix", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 347 + Text("Play Mix", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 346 348 347 349 if (modeCounts.isNotEmpty() || diffAverages.size >= 3) { 348 350 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Top) { 349 351 if (modeCounts.isNotEmpty()) { 350 352 Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { 351 - Text("Chart Type", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 353 + Text("Chart Type", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 352 354 TransparentPieChart(modeCounts, modifier = Modifier.fillMaxWidth().height(150.dp)) 353 355 modeCounts.forEach { (label, count, color) -> 354 356 LegendRow(label, count, color) ··· 357 359 } 358 360 if (diffAverages.size >= 3) { 359 361 Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { 360 - Text("Avg by Diff", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 362 + Text("Avg by Diff", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 361 363 DifficultyRadarChart(diffAverages, modifier = Modifier.fillMaxWidth().height(180.dp)) 362 364 } 363 365 } ··· 365 367 } 366 368 367 369 if (rankCounts.isNotEmpty()) { 368 - Text("Rank Distribution", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 370 + Text("Rank Distribution", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 369 371 TransparentBarChart(rankCounts, modifier = Modifier.fillMaxWidth().height(220.dp)) 370 372 } 371 373 } ··· 381 383 scaleStepsCount = 4, 382 384 maxLabelWidth = 42.dp, 383 385 radarChartLineStyle = RadarChartLineStyle( 384 - color = MutedText.copy(alpha = 0.25f), 386 + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f), 385 387 strokeWidth = 1f, 386 388 strokeCap = StrokeCap.Butt, 387 389 ), 388 390 radarChartDataSet = listOf( 389 391 RadarChartDataSet( 390 392 style = RadarChartDataSetStyle( 391 - color = PrimaryBlue, 392 - borderColor = AccentPink, 393 + color = MaterialTheme.colorScheme.primary, 394 + borderColor = MaterialTheme.colorScheme.secondary, 393 395 colorAlpha = 0.2f, 394 396 strokeWidth = 2f, 395 397 strokeCap = StrokeCap.Round, ··· 397 399 data = diffAverages.map { it.second }, 398 400 ), 399 401 ), 400 - labelTextStyle = androidx.compose.ui.text.TextStyle(color = DarkText, fontSize = 9.sp, fontWeight = FontWeight.SemiBold), 402 + labelTextStyle = androidx.compose.ui.text.TextStyle(color = MaterialTheme.colorScheme.onSurface, fontSize = 9.sp, fontWeight = FontWeight.SemiBold), 401 403 scaleTextStyle = androidx.compose.ui.text.TextStyle(color = Color.Transparent, fontSize = 1.sp), 402 404 scaleUnit = "%", 403 405 maxScaleValue = 101.0, ··· 409 411 @Composable 410 412 private fun TransparentLineChart(trend: List<AverageBucket>, modifier: Modifier = Modifier, unit: String = "%") { 411 413 Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 414 + val primaryColor = MaterialTheme.colorScheme.primary 415 + val gridColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) 412 416 val minValue = trend.minOf { it.average }.coerceAtMost(100.0) 413 417 val maxValue = trend.maxOf { it.average }.coerceAtLeast(minValue + 0.1) 414 418 val axisMin = ((minValue - 0.25).coerceAtLeast(0.0) * 10).toInt() / 10.0 415 419 val axisMax = (((maxValue + 0.25).coerceAtMost(101.0) * 10).toInt() + 1) / 10.0 416 420 Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(6.dp)) { 417 421 Column(modifier = Modifier.width(34.dp).fillMaxHeight().padding(top = 2.dp, bottom = 24.dp), verticalArrangement = Arrangement.SpaceBetween) { 418 - Text("${formatScore(axisMax, 1)}$unit", fontSize = 9.sp, color = MutedText, textAlign = TextAlign.End, modifier = Modifier.fillMaxWidth()) 419 - Text("${formatScore((axisMin + axisMax) / 2.0, 1)}$unit", fontSize = 9.sp, color = MutedText, textAlign = TextAlign.End, modifier = Modifier.fillMaxWidth()) 420 - Text("${formatScore(axisMin, 1)}$unit", fontSize = 9.sp, color = MutedText, textAlign = TextAlign.End, modifier = Modifier.fillMaxWidth()) 422 + Text("${formatScore(axisMax, 1)}$unit", fontSize = 9.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.End, modifier = Modifier.fillMaxWidth()) 423 + Text("${formatScore((axisMin + axisMax) / 2.0, 1)}$unit", fontSize = 9.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.End, modifier = Modifier.fillMaxWidth()) 424 + Text("${formatScore(axisMin, 1)}$unit", fontSize = 9.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.End, modifier = Modifier.fillMaxWidth()) 421 425 } 422 426 Canvas(modifier = Modifier.weight(1f).fillMaxHeight()) { 423 427 val left = 0f ··· 427 431 428 432 repeat(4) { i -> 429 433 val y = top + ((bottom - top) * i / 3f) 430 - drawLine(MutedText.copy(alpha = 0.16f), start = androidx.compose.ui.geometry.Offset(left, y), end = androidx.compose.ui.geometry.Offset(right, y), strokeWidth = 1.dp.toPx()) 434 + drawLine(gridColor, start = androidx.compose.ui.geometry.Offset(left, y), end = androidx.compose.ui.geometry.Offset(right, y), strokeWidth = 1.dp.toPx()) 431 435 } 432 436 433 437 val points = trend.mapIndexed { index, bucket -> ··· 439 443 moveTo(points.first().x, points.first().y) 440 444 points.drop(1).forEach { lineTo(it.x, it.y) } 441 445 } 442 - drawPath(path, color = PrimaryBlue, style = Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round)) 446 + drawPath(path, color = primaryColor, style = Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round)) 443 447 } 444 448 } 445 449 Row(modifier = Modifier.fillMaxWidth().padding(start = 40.dp), horizontalArrangement = Arrangement.SpaceBetween) { 446 - Text(trend.first().label, fontSize = 10.sp, color = MutedText) 447 - Text(trend.last().label, fontSize = 10.sp, color = MutedText) 450 + Text(trend.first().label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 451 + Text(trend.last().label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 448 452 } 449 453 } 450 454 } ··· 452 456 @Composable 453 457 private fun TransparentPieChart(slices: List<Triple<String, Int, Color>>, modifier: Modifier = Modifier) { 454 458 val total = slices.sumOf { it.second }.coerceAtLeast(1) 459 + val centerCutoutColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f) 455 460 Canvas(modifier = modifier) { 456 461 val diameter = minOf(size.width, size.height) * 0.86f 457 462 val topLeft = androidx.compose.ui.geometry.Offset((size.width - diameter) / 2f, (size.height - diameter) / 2f) ··· 461 466 drawArc(color = color, startAngle = start, sweepAngle = sweep, useCenter = true, topLeft = topLeft, size = androidx.compose.ui.geometry.Size(diameter, diameter)) 462 467 start += sweep 463 468 } 464 - drawCircle(CardWhite.copy(alpha = 0.9f), radius = diameter * 0.28f, center = center) 469 + drawCircle(centerCutoutColor, radius = diameter * 0.28f, center = center) 465 470 } 466 471 } 467 472 468 473 @Composable 469 474 private fun TransparentBarChart(rankCounts: List<RankCount>, modifier: Modifier = Modifier) { 470 475 Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 476 + val gridColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) 471 477 Box(modifier = modifier) { 472 478 Canvas(modifier = Modifier.matchParentSize()) { 473 479 val maxCount = (rankCounts.maxOfOrNull { it.count } ?: 1).coerceAtLeast(1) ··· 477 483 val barWidth = (slot * 0.55f).coerceAtMost(24.dp.toPx()) 478 484 repeat(4) { i -> 479 485 val y = top + ((bottom - top) * i / 3f) 480 - drawLine(MutedText.copy(alpha = 0.14f), start = androidx.compose.ui.geometry.Offset(0f, y), end = androidx.compose.ui.geometry.Offset(size.width, y), strokeWidth = 1.dp.toPx()) 486 + drawLine(gridColor, start = androidx.compose.ui.geometry.Offset(0f, y), end = androidx.compose.ui.geometry.Offset(size.width, y), strokeWidth = 1.dp.toPx()) 481 487 } 482 488 rankCounts.forEachIndexed { index, item -> 483 489 val height = (bottom - top) * item.count / maxCount.toFloat() ··· 499 505 "${item.count}", 500 506 fontSize = 10.sp, 501 507 fontWeight = FontWeight.Bold, 502 - color = DarkText, 508 + color = MaterialTheme.colorScheme.onSurface, 503 509 textAlign = TextAlign.Center, 504 510 modifier = Modifier 505 511 .align(Alignment.BottomCenter) ··· 514 520 } ?: Text( 515 521 item.label, 516 522 fontSize = 9.sp, 517 - color = MutedText, 523 + color = MaterialTheme.colorScheme.onSurfaceVariant, 518 524 textAlign = TextAlign.Center, 519 525 modifier = Modifier.align(Alignment.BottomCenter), 520 526 ) ··· 528 534 @Composable 529 535 private fun LegendRow(label: String, count: Int, color: Color) { 530 536 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { 531 - Surface(modifier = Modifier.size(10.dp), shape = RoundedCornerShape(2.dp), color = color, content = {}) 532 - Text(label, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = DarkText) 533 - Text("$count", fontSize = 12.sp, color = MutedText) 537 + ExpressiveDot(color = color) 538 + Text(label, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) 539 + Text("$count", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 534 540 } 535 541 } 536 542 ··· 626 632 return WeekBucketKey(year, month, week) 627 633 } 628 634 629 - private fun List<PlayStore.HistoricalPlay>.modeCounts(): List<Triple<String, Int, Color>> { 635 + private fun List<PlayStore.HistoricalPlay>.modeCounts(primaryColor: Color): List<Triple<String, Int, Color>> { 630 636 val dx = count { it.isDx } 631 637 val standard = size - dx 638 + val colorsByMode = mapOf( 639 + "DX" to primaryColor, 640 + "Standard" to DiffAdvanced, 641 + ) 632 642 return listOf("DX" to dx, "Standard" to standard) 633 643 .filter { it.second > 0 } 634 - .map { (label, count) -> Triple(label, count, if (label == "DX") PrimaryBlue else AccentPink) } 644 + .map { (label, count) -> Triple(label, count, colorsByMode[label] ?: primaryColor) } 635 645 } 636 646 637 - private fun List<PlayStore.HistoricalPlay>.rankCounts(): List<RankCount> { 647 + private fun List<PlayStore.HistoricalPlay>.rankCounts(primaryColor: Color): List<RankCount> { 638 648 val order = listOf("sssp", "sss", "ssp", "ss", "sp", "s", "aaa", "aa", "a", "bbb", "bb", "b", "c", "d") 639 - val colors = listOf(AccentPink, PrimaryBlue, DiffMaster, DiffExpert, DiffAdvanced, DiffBasic) 649 + val colors = listOf(primaryColor, DiffMaster, DiffExpert, DiffAdvanced, DiffBasic) 640 650 return groupBy { it.scoreRank.normalizedRank() } 641 651 .filterKeys { it.isNotBlank() } 642 652 .mapValues { (_, plays) -> plays.size }
+22 -21
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/PlayerNameTab.kt
··· 2 2 3 3 import androidx.compose.foundation.layout.* 4 4 import androidx.compose.foundation.rememberScrollState 5 - import androidx.compose.foundation.shape.RoundedCornerShape 6 5 import androidx.compose.foundation.verticalScroll 7 6 import androidx.compose.material.icons.Icons 8 7 import androidx.compose.material.icons.filled.Check ··· 17 16 import com.derakkuma.atproto.syncCachedProfileToAtProto 18 17 import com.derakkuma.data.* 19 18 import com.derakkuma.ui.components.EmptyState 19 + import com.derakkuma.ui.components.ExpressiveGlassCard 20 + import com.derakkuma.ui.components.ExpressiveTextButton 21 + import com.derakkuma.ui.components.ExpressiveSnackbar 22 + import com.derakkuma.ui.components.ExpressiveScaffold 23 + import com.derakkuma.ui.components.ExpressiveFloatingActionButton 24 + import com.derakkuma.ui.components.ExpressiveTextField 20 25 import com.derakkuma.ui.components.LoadingState 21 26 import com.derakkuma.ui.theme.* 22 27 import kotlinx.coroutines.launch ··· 53 58 loading = false 54 59 } 55 60 56 - Scaffold( 61 + ExpressiveScaffold( 57 62 contentWindowInsets = WindowInsets(0.dp), 58 63 snackbarHost = { 59 64 if (snackMsg != null) { 60 - Snackbar( 65 + ExpressiveSnackbar( 61 66 modifier = Modifier.padding(12.dp), 62 - shape = RoundedCornerShape(8.dp), 63 - action = { TextButton(onClick = { snackMsg = null }) { Text("OK") } }, 67 + action = { ExpressiveTextButton(onClick = { snackMsg = null }) { Text("OK") } }, 64 68 ) { Text(snackMsg!!) } 65 69 } 66 70 }, 67 71 floatingActionButton = { 68 72 if (dirty && !saving && !maintenanceMode) { 69 - FloatingActionButton( 73 + ExpressiveFloatingActionButton( 70 74 onClick = { 71 - val settings = nameSettings ?: return@FloatingActionButton 75 + val settings = nameSettings ?: return@ExpressiveFloatingActionButton 72 76 saving = true 73 77 scope.launch { 74 78 try { ··· 89 93 saving = false 90 94 } 91 95 }, 92 - containerColor = PrimaryBlue, 96 + containerColor = MaterialTheme.colorScheme.primary, 93 97 ) { 94 - Icon(Icons.Default.Check, contentDescription = "Save", tint = CardWhite) 98 + Icon(Icons.Default.Check, contentDescription = "Save", tint = MaterialTheme.colorScheme.onPrimary) 95 99 } 96 100 } 97 101 }, ··· 110 114 } else if (nameSettings != null) { 111 115 val ns = nameSettings!! 112 116 113 - Card( 117 + ExpressiveGlassCard( 114 118 modifier = Modifier.fillMaxWidth(), 115 - shape = RoundedCornerShape(8.dp), 116 - colors = CardDefaults.cardColors(containerColor = CardWhite), 117 119 ) { 118 120 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { 119 - Text("Player Name", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = DarkText) 121 + Text("Player Name", fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 120 122 121 - OutlinedTextField( 123 + ExpressiveTextField( 122 124 value = newName, 123 125 onValueChange = { 124 - if (maintenanceMode) return@OutlinedTextField 126 + if (maintenanceMode) return@ExpressiveTextField 125 127 if (it.length <= ns.maxLength) { 126 128 newName = it 127 129 dirty = it != ns.currentName ··· 132 134 singleLine = true, 133 135 enabled = !maintenanceMode, 134 136 supportingText = { Text("${newName.length} / ${ns.maxLength}") }, 135 - shape = RoundedCornerShape(8.dp), 136 - ) 137 + ) 137 138 138 139 if (ns.allowedSymbols.isNotEmpty()) { 139 - Text("Additional Symbols", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MutedText) 140 + Text("Additional Symbols", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant) 140 141 // Symbol grid 141 142 FlowRow( 142 143 modifier = Modifier.fillMaxWidth(), ··· 144 145 verticalArrangement = Arrangement.spacedBy(4.dp), 145 146 ) { 146 147 ns.allowedSymbols.forEach { symbol -> 147 - TextButton( 148 + ExpressiveTextButton( 148 149 onClick = { 149 - if (maintenanceMode) return@TextButton 150 + if (maintenanceMode) return@ExpressiveTextButton 150 151 if (newName.length + symbol.length <= ns.maxLength) { 151 152 newName += symbol 152 153 dirty = newName != ns.currentName ··· 156 157 enabled = !maintenanceMode, 157 158 contentPadding = PaddingValues(0.dp), 158 159 ) { 159 - Text(symbol, fontSize = 16.sp, color = PrimaryBlue) 160 + Text(symbol, fontSize = 16.sp, color = MaterialTheme.colorScheme.primary) 160 161 } 161 162 } 162 163 }
+110 -71
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/RankingTab.kt
··· 3 3 import androidx.compose.foundation.layout.* 4 4 import androidx.compose.foundation.lazy.LazyColumn 5 5 import androidx.compose.foundation.lazy.items 6 + import androidx.compose.foundation.shape.CircleShape 6 7 import androidx.compose.foundation.shape.RoundedCornerShape 7 8 import androidx.compose.material3.* 8 9 import androidx.compose.runtime.* ··· 15 16 import androidx.compose.ui.unit.sp 16 17 import com.derakkuma.data.* 17 18 import com.derakkuma.ui.components.ErrorState 19 + import com.derakkuma.ui.components.ExpressiveButton 20 + import com.derakkuma.ui.components.ExpressiveGlassCard 21 + import com.derakkuma.ui.components.ExpressiveBadgeSurface 22 + import com.derakkuma.ui.components.ExpressiveFilterChip 23 + import com.derakkuma.ui.components.ExpressiveTextButton 24 + import com.derakkuma.ui.components.ExpressiveDropdownMenu 25 + import com.derakkuma.ui.components.ExpressiveDropdownMenuItem 18 26 import com.derakkuma.ui.components.LoadingState 19 27 import com.derakkuma.ui.theme.* 20 28 import com.derakkuma.util.formatScore ··· 124 132 125 133 Column(Modifier.fillMaxSize()) { 126 134 // Sub-section tabs 127 - PrimaryScrollableTabRow( 128 - selectedTabIndex = section.ordinal, 129 - containerColor = Color.Transparent, 130 - contentColor = Color.White, 131 - edgePadding = 1.dp, 132 - divider = {}, 133 - ) { 134 - RankingSection.entries.forEach { s -> 135 - Tab( 136 - selected = section == s, 137 - onClick = { section = s }, 138 - text = { Text(s.label, fontSize = 12.sp, fontWeight = if (section == s) FontWeight.Bold else FontWeight.Normal) }, 139 - ) 140 - } 141 - } 135 + RankingSectionTabs( 136 + selected = section, 137 + onSelect = { section = it }, 138 + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), 139 + ) 142 140 143 141 if (loading) { 144 - LoadingState(Modifier.fillMaxSize(), color = PrimaryBlue) 142 + LoadingState(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) 145 143 } else if (error != null) { 146 144 ErrorState(error!!, onRetry = { reload() }) 147 145 } else { ··· 208 206 } 209 207 } 210 208 209 + @Composable 210 + private fun RankingSectionTabs( 211 + selected: RankingSection, 212 + onSelect: (RankingSection) -> Unit, 213 + modifier: Modifier = Modifier, 214 + ) { 215 + ExpressiveGlassCard(modifier = modifier, cornerRadius = 26.dp) { 216 + Column( 217 + modifier = Modifier.fillMaxWidth().padding(12.dp), 218 + verticalArrangement = Arrangement.spacedBy(8.dp), 219 + ) { 220 + Text("Ranking board", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant) 221 + FlowRow( 222 + modifier = Modifier.fillMaxWidth(), 223 + horizontalArrangement = Arrangement.spacedBy(6.dp), 224 + verticalArrangement = Arrangement.spacedBy(6.dp), 225 + ) { 226 + RankingSection.entries.forEach { option -> 227 + RankingExpressiveFilterChip( 228 + selected = option == selected, 229 + selectedColor = when (option) { 230 + RankingSection.Song -> MaterialTheme.colorScheme.primary 231 + RankingSection.Course -> MaterialTheme.colorScheme.tertiary 232 + RankingSection.Season -> MaterialTheme.colorScheme.secondary 233 + RankingSection.DeluxeRating -> MaterialTheme.colorScheme.error 234 + RankingSection.Total -> MaterialTheme.colorScheme.primary 235 + RankingSection.Partner -> MaterialTheme.colorScheme.tertiary 236 + RankingSection.Finale -> MaterialTheme.colorScheme.secondary 237 + }, 238 + onClick = { onSelect(option) }, 239 + ) { 240 + Text(option.label, fontSize = 11.sp, fontWeight = if (option == selected) FontWeight.Bold else FontWeight.SemiBold, maxLines = 1) 241 + } 242 + } 243 + } 244 + } 245 + } 246 + } 247 + 211 248 // ---- Filter Chips ---- 212 249 213 250 @Composable 214 251 private fun FilterChipRow(label: String, options: List<RankingFilterOption>, selected: String, onSelect: (String) -> Unit) { 215 252 if (options.isEmpty()) return 216 253 Column(Modifier.fillMaxWidth().padding(vertical = 4.dp)) { 217 - Text(label, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MutedText, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)) 254 + Text(label, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)) 218 255 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) { 219 256 options.take(8).forEach { opt -> 220 - RankingFilterChip(selected = opt.value == selected, onClick = { onSelect(opt.value) }) { Text(opt.label, fontSize = 11.sp, maxLines = 1) } 257 + RankingExpressiveFilterChip(selected = opt.value == selected, onClick = { onSelect(opt.value) }) { Text(opt.label, fontSize = 11.sp, maxLines = 1) } 221 258 } 222 259 if (options.size > 8) { 223 260 // Show dropdown for too many options 224 261 var expanded by remember { mutableStateOf(false) } 225 262 Box { 226 - TextButton(onClick = { expanded = true }, modifier = Modifier.height(32.dp)) { 227 - Text("More...", fontSize = 11.sp, color = PrimaryBlue) 263 + ExpressiveTextButton(onClick = { expanded = true }, modifier = Modifier.height(32.dp)) { 264 + Text("More...", fontSize = 11.sp, color = MaterialTheme.colorScheme.primary) 228 265 } 229 - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { 230 - options.drop(8).forEach { opt -> 231 - DropdownMenuItem( 232 - text = { Text(opt.label, fontSize = 12.sp) }, 266 + ExpressiveDropdownMenu( 267 + expanded = expanded, 268 + onDismissRequest = { expanded = false }, 269 + ) { 270 + val extraOptions = options.drop(8) 271 + extraOptions.forEachIndexed { index, opt -> 272 + val isSelected = opt.value == selected 273 + ExpressiveDropdownMenuItem( 274 + selected = isSelected, 275 + text = { Text(opt.label, fontSize = 12.sp, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal) }, 276 + leadingIcon = if (isSelected) ({ Text("✓", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) }) else null, 277 + index = index, 278 + lastIndex = extraOptions.lastIndex, 233 279 onClick = { 234 280 onSelect(opt.value) 235 281 expanded = false ··· 244 290 } 245 291 246 292 @Composable 247 - private fun RankingFilterChip(selected: Boolean, onClick: () -> Unit, label: @Composable () -> Unit) { 248 - FilterChip( 293 + private fun RankingExpressiveFilterChip( 294 + selected: Boolean, 295 + onClick: () -> Unit, 296 + selectedColor: Color = MaterialTheme.colorScheme.primary, 297 + label: @Composable () -> Unit, 298 + ) { 299 + ExpressiveFilterChip( 249 300 selected = selected, 250 301 onClick = onClick, 251 302 label = label, 252 - modifier = Modifier.height(32.dp), 253 - colors = FilterChipDefaults.filterChipColors( 254 - containerColor = Color.White, 255 - labelColor = DarkText, 256 - selectedContainerColor = PrimaryBlue, 257 - selectedLabelColor = Color.White, 258 - ), 259 - border = FilterChipDefaults.filterChipBorder( 260 - enabled = true, 261 - selected = selected, 262 - borderColor = Color.Transparent, 263 - selectedBorderColor = PrimaryBlue, 264 - ), 303 + selectedColor = selectedColor, 265 304 ) 266 305 } 267 306 ··· 271 310 Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(6.dp)) { 272 311 options.forEach { opt -> 273 312 val chipColor = when (opt.value) { 274 - "0" -> Color(0xFF22A7F0) 313 + "0" -> MaterialTheme.colorScheme.primary 275 314 276 315 // Basic - blue 277 - "1" -> Color(0xFF2ECC71) 316 + "1" -> DiffBasic 278 317 279 318 // Advanced - green 280 - "2" -> Color(0xFFFFC312) 319 + "2" -> DiffAdvanced 281 320 282 321 // Expert - yellow 283 - "3" -> Color(0xFFE74C3C) 322 + "3" -> DiffExpert 284 323 285 324 // Master - red 286 - "4" -> Color(0xFF9B59B6) 325 + "4" -> DiffMaster 287 326 288 327 // Re:MASTER - purple 289 - "10" -> Color(0xFFE91E63) 328 + "10" -> DiffRemaster 290 329 291 330 // Utage - pink 292 - "99" -> Color(0xFF7F8C8D) 331 + "99" -> MaterialTheme.colorScheme.outline 293 332 294 333 // All - gray 295 - else -> PrimaryBlue 334 + else -> MaterialTheme.colorScheme.primary 296 335 } 297 - RankingFilterChip(selected = opt.value == selected, onClick = { onSelect(opt.value) }) { Text(opt.label, fontSize = 11.sp, maxLines = 1) } 336 + RankingExpressiveFilterChip(selected = opt.value == selected, selectedColor = chipColor, onClick = { onSelect(opt.value) }) { Text(opt.label, fontSize = 11.sp, maxLines = 1) } 298 337 } 299 338 } 300 339 } ··· 317 356 318 357 @Composable 319 358 private fun ApplyButton(onClick: () -> Unit) { 320 - Button(onClick = onClick, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)) { 359 + ExpressiveButton(onClick = onClick, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)) { 321 360 Text("Apply Filters", fontWeight = FontWeight.SemiBold) 322 361 } 323 362 } ··· 329 368 LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 88.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { 330 369 if (yourRank.isNotBlank() && yourRank != "―") { 331 370 item(key = "your-rank") { 332 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = PrimaryBlue.copy(alpha = 0.1f))) { 371 + ExpressiveGlassCard(Modifier.fillMaxWidth(), cornerRadius = 22.dp, containerColor = MaterialTheme.colorScheme.primaryContainer) { 333 372 Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) { 334 - Text("Your Rank: ", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue) 335 - Text(yourRank, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = PrimaryBlue) 373 + Text("Your Rank: ", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onPrimaryContainer) 374 + Text(yourRank, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimaryContainer) 336 375 } 337 376 } 338 377 } 339 378 } 340 379 if (updatedAt.isNotBlank()) { 341 380 item(key = "updated") { 342 - Text("Updated: $updatedAt", fontSize = 11.sp, color = MutedText, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)) 381 + Text("Updated: $updatedAt", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)) 343 382 } 344 383 } 345 384 if (headerLabel.isNotBlank()) { 346 385 item(key = "header-label") { 347 - Text(headerLabel, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)) 386 + Text(headerLabel, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)) 348 387 } 349 388 } 350 389 items(entries, key = { "pr-${it.rank}-${it.name}" }) { entry -> ··· 356 395 @Composable 357 396 private fun PlayerRankingRow(entry: PlayerRankingEntry) { 358 397 val rankColor = when { 359 - entry.rank == 1 -> Color(0xFFFFD700) 360 - entry.rank == 2 -> Color(0xFFC0C0C0) 361 - entry.rank == 3 -> Color(0xFFCD7F32) 362 - entry.isTop3 -> Color(0xFFFFD700) 398 + entry.rank == 1 -> MedalGold 399 + entry.rank == 2 -> MedalSilver 400 + entry.rank == 3 -> MedalBronze 401 + entry.isTop3 -> MedalGold 363 402 else -> MutedText.copy(alpha = 0.3f) 364 403 } 365 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 404 + ExpressiveGlassCard(Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 366 405 Row(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) { 367 406 Surface(Modifier.size(28.dp), shape = RoundedCornerShape(6.dp), color = rankColor) { 368 407 Box(contentAlignment = Alignment.Center) { ··· 370 409 } 371 410 } 372 411 Spacer(Modifier.width(8.dp)) 373 - Text(entry.name, fontSize = 14.sp, fontWeight = if (entry.isTop3) FontWeight.SemiBold else FontWeight.Normal, color = DarkText, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) 412 + Text(entry.name, fontSize = 14.sp, fontWeight = if (entry.isTop3) FontWeight.SemiBold else FontWeight.Normal, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) 374 413 Spacer(Modifier.width(8.dp)) 375 - Text(entry.score, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = if (entry.isTop3) PrimaryBlue else MutedText) 414 + Text(entry.score, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = if (entry.isTop3) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant) 376 415 } 377 416 } 378 417 } ··· 430 469 if (song.genre != lastGenre && song.genre.isNotBlank()) { 431 470 lastGenre = song.genre 432 471 item(key = "genre-$i") { 433 - Text(song.genre, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = PrimaryBlue, modifier = Modifier.padding(start = 4.dp, top = 8.dp, bottom = 4.dp)) 472 + Text(song.genre, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 4.dp, top = 8.dp, bottom = 4.dp)) 434 473 } 435 474 } 436 475 item(key = "song-$i") { ··· 440 479 } 441 480 } else if (data != null && data.songs.isEmpty()) { 442 481 Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) { 443 - Text("No songs found for these filters", fontSize = 14.sp, color = MutedText) 482 + Text("No songs found for these filters", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 444 483 } 445 484 } 446 485 } ··· 455 494 "remaster" -> Color(0xFF9B59B6) 456 495 else -> MutedText 457 496 } 458 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = CardWhite)) { 497 + ExpressiveGlassCard(Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 459 498 Row(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) { 460 499 // Difficulty badge 461 500 Surface(Modifier.size(28.dp), shape = RoundedCornerShape(6.dp), color = diffColor.copy(alpha = 0.2f)) { ··· 465 504 } 466 505 Spacer(Modifier.width(8.dp)) 467 506 // Song name 468 - Text(song.name, fontSize = 14.sp, fontWeight = FontWeight.Normal, color = DarkText, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) 507 + Text(song.name, fontSize = 14.sp, fontWeight = FontWeight.Normal, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) 469 508 // DX badge 470 509 if (song.isDx) { 471 510 Spacer(Modifier.width(4.dp)) 472 - Surface(shape = RoundedCornerShape(4.dp), color = Color(0xFFE91E63).copy(alpha = 0.15f)) { 473 - Text("DX", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color(0xFFE91E63), modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)) 511 + ExpressiveBadgeSurface(shape = MaterialTheme.shapes.large, containerColor = MaterialTheme.colorScheme.secondaryContainer) { 512 + Text("DX", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)) 474 513 } 475 514 } 476 515 } ··· 567 606 // Your rating 568 607 if (data.yourRating > 0) { 569 608 item(key = "your-rating") { 570 - Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = PrimaryBlue.copy(alpha = 0.1f))) { 609 + ExpressiveGlassCard(Modifier.fillMaxWidth(), cornerRadius = 22.dp, containerColor = MaterialTheme.colorScheme.primaryContainer) { 571 610 Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) { 572 - Text("Your DX Rating: ", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = PrimaryBlue) 573 - Text("${data.yourRating}", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = PrimaryBlue) 611 + Text("Your DX Rating: ", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onPrimaryContainer) 612 + Text("${data.yourRating}", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimaryContainer) 574 613 } 575 614 } 576 615 } 577 616 } 578 617 if (data.updatedAt.isNotBlank()) { 579 - item(key = "updated") { Text("Updated: ${data.updatedAt}", fontSize = 11.sp, color = MutedText, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)) } 618 + item(key = "updated") { Text("Updated: ${data.updatedAt}", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp)) } 580 619 } 581 620 items(data.entries, key = { "dx-${it.rank}-${it.name}" }) { entry -> PlayerRankingRow(entry) } 582 621 }
+13 -28
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/RatingTab.kt
··· 3 3 import androidx.compose.foundation.layout.* 4 4 import androidx.compose.foundation.lazy.LazyColumn 5 5 import androidx.compose.foundation.lazy.itemsIndexed 6 - import androidx.compose.foundation.shape.RoundedCornerShape 7 6 import androidx.compose.material3.* 8 - import androidx.compose.material3.pulltorefresh.PullToRefreshBox 9 7 import androidx.compose.runtime.* 10 8 import androidx.compose.ui.Alignment 11 9 import androidx.compose.ui.Modifier ··· 21 19 import com.derakkuma.data.RatingSong 22 20 import com.derakkuma.data.SessionExpiredException 23 21 import com.derakkuma.ui.components.ErrorState 22 + import com.derakkuma.ui.components.ExpressiveGlassCard 23 + import com.derakkuma.ui.components.ExpressivePullToRefreshBox 24 + import com.derakkuma.ui.components.ExpressiveHorizontalDivider 24 25 import com.derakkuma.ui.components.LoadingState 25 26 import com.derakkuma.ui.components.SongRow 26 27 import com.derakkuma.ui.theme.* ··· 64 65 isRefreshing = false 65 66 } 66 67 67 - PullToRefreshBox( 68 + ExpressivePullToRefreshBox( 68 69 isRefreshing = isRefreshing, 69 70 onRefresh = { 70 - if (maintenanceMode) return@PullToRefreshBox 71 + if (maintenanceMode) return@ExpressivePullToRefreshBox 71 72 isRefreshing = true 72 73 refreshKey += 1 73 74 }, ··· 83 84 LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(start = 12.dp, end = 12.dp, top = 6.dp, bottom = 88.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { 84 85 // New Songs section 85 86 item { 86 - Card( 87 - modifier = Modifier.fillMaxWidth(), 88 - shape = RoundedCornerShape(8.dp), 89 - colors = CardDefaults.cardColors(containerColor = CardWhite), 90 - ) { 91 - Text("New Songs (${newSongs.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(10.dp).padding(bottom = 4.dp)) 87 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 22.dp) { 88 + Text("New Songs (${newSongs.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(10.dp).padding(bottom = 4.dp)) 92 89 } 93 90 } 94 91 itemsIndexed(newSongs, key = { index, song -> "new-$index-${song.name}-${song.difficulty}" }) { i, song -> 95 - Card( 96 - modifier = Modifier.fillMaxWidth(), 97 - shape = RoundedCornerShape(8.dp), 98 - colors = CardDefaults.cardColors(containerColor = CardWhite), 99 - ) { 92 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 22.dp) { 100 93 Column(modifier = Modifier.padding(horizontal = 10.dp)) { 101 - if (i > 0) HorizontalDivider(color = MutedText.copy(alpha = 0.15f)) 94 + if (i > 0) ExpressiveHorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) 102 95 RatingSongRow(song) 103 96 } 104 97 } ··· 106 99 107 100 // Other Songs section 108 101 item { 109 - Card( 110 - modifier = Modifier.fillMaxWidth(), 111 - shape = RoundedCornerShape(8.dp), 112 - colors = CardDefaults.cardColors(containerColor = CardWhite), 113 - ) { 114 - Text("Other Songs (${otherSongs.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(10.dp).padding(bottom = 4.dp)) 102 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 22.dp) { 103 + Text("Other Songs (${otherSongs.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(10.dp).padding(bottom = 4.dp)) 115 104 } 116 105 } 117 106 itemsIndexed(otherSongs, key = { index, song -> "other-$index-${song.name}-${song.difficulty}" }) { i, song -> 118 - Card( 119 - modifier = Modifier.fillMaxWidth(), 120 - shape = RoundedCornerShape(8.dp), 121 - colors = CardDefaults.cardColors(containerColor = CardWhite), 122 - ) { 107 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 22.dp) { 123 108 Column(modifier = Modifier.padding(horizontal = 10.dp)) { 124 - if (i > 0) HorizontalDivider(color = MutedText.copy(alpha = 0.15f)) 109 + if (i > 0) ExpressiveHorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) 125 110 RatingSongRow(song) 126 111 } 127 112 }
+61 -75
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/ScoresTab.kt
··· 5 5 import androidx.compose.foundation.lazy.LazyColumn 6 6 import androidx.compose.foundation.lazy.items 7 7 import androidx.compose.foundation.lazy.rememberLazyListState 8 - import androidx.compose.foundation.shape.RoundedCornerShape 9 8 import androidx.compose.material.icons.Icons 10 9 import androidx.compose.material.icons.filled.Sync 11 10 import androidx.compose.material3.* 12 - import androidx.compose.material3.pulltorefresh.PullToRefreshBox 13 11 import androidx.compose.runtime.* 14 12 import androidx.compose.ui.Alignment 15 13 import androidx.compose.ui.Modifier ··· 30 28 import com.derakkuma.data.RecentPlay 31 29 import com.derakkuma.data.SessionExpiredException 32 30 import com.derakkuma.ui.components.ErrorState 31 + import com.derakkuma.ui.components.ExpressiveButton 32 + import com.derakkuma.ui.components.ExpressiveGlassCard 33 + import com.derakkuma.ui.components.ExpressivePullToRefreshBox 34 + import com.derakkuma.ui.components.ExpressiveHorizontalDivider 35 + import com.derakkuma.ui.components.ExpressiveBadgeSurface 36 + import com.derakkuma.ui.components.ExpressiveLinearProgressIndicator 37 + import com.derakkuma.ui.components.ExpressiveLoadingIndicator 33 38 import com.derakkuma.ui.components.LoadingState 34 39 import com.derakkuma.ui.components.SongRow 35 40 import com.derakkuma.ui.images.BundledIcon ··· 141 146 historicalPlays = ps.getHistoricalPlaysForDisplay() 142 147 } 143 148 144 - PullToRefreshBox( 149 + ExpressivePullToRefreshBox( 145 150 isRefreshing = isRefreshing, 146 151 onRefresh = { 147 - if (maintenanceMode) return@PullToRefreshBox 152 + if (maintenanceMode) return@ExpressivePullToRefreshBox 148 153 isRefreshing = true 149 154 refreshKey += 1 150 155 }, ··· 189 194 ) { 190 195 if (loading) { 191 196 item { 192 - LinearProgressIndicator( 197 + ExpressiveLinearProgressIndicator( 193 198 modifier = Modifier.fillMaxWidth().height(2.dp), 194 - color = PrimaryBlue, 199 + color = MaterialTheme.colorScheme.primary, 195 200 trackColor = Color.Transparent, 196 201 ) 197 202 } 198 203 } 199 204 200 - // Stats summary 205 + // Stats summary. Keep the original records summary as the first real content 206 + // item so Current Ver / Dashboard / Archived / maimile remain visible at top. 201 207 item { 202 - Card( 208 + ExpressiveGlassCard( 203 209 modifier = Modifier.fillMaxWidth(), 204 - shape = RoundedCornerShape(8.dp), 205 - colors = CardDefaults.cardColors(containerColor = CardWhite), 210 + cornerRadius = 28.dp, 211 + containerColor = Color.White, 206 212 ) { 207 - Row( 213 + Column( 208 214 modifier = Modifier.fillMaxWidth().padding(12.dp), 209 - horizontalArrangement = Arrangement.SpaceEvenly, 215 + verticalArrangement = Arrangement.spacedBy(12.dp), 210 216 ) { 211 - StatItem("Current Ver", "${data!!.playCount.currentVersion}") 212 - StatItem("Dashboard", "${data!!.playCount.total}") 213 - StatItem( 214 - "Archived", 215 - "${historicalPlays.size}", 216 - showSyncChip = historicalPlays.isEmpty(), 217 - onClick = { scope.launch { listState.animateScrollToItem(historicalSectionIndex) } }, 218 - ) 219 - StatItem("maimile", "${data!!.maimile}", useMaimileIcon = true) 220 - } 217 + Text("Play Statistics", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 218 + Row( 219 + modifier = Modifier.fillMaxWidth(), 220 + horizontalArrangement = Arrangement.SpaceEvenly, 221 + ) { 222 + StatItem("Current Ver", "${data!!.playCount.currentVersion}") 223 + StatItem("Dashboard", "${data!!.playCount.total}") 224 + StatItem( 225 + "Archived", 226 + "${historicalPlays.size}", 227 + showSyncChip = historicalPlays.isEmpty(), 228 + onClick = { scope.launch { listState.animateScrollToItem(historicalSectionIndex) } }, 229 + ) 230 + StatItem("maimile", "${data!!.maimile}", useMaimileIcon = true) 231 + } 221 232 222 - // Rating target music shortcut 223 - Column( 224 - modifier = Modifier.fillMaxWidth().padding(10.dp), 225 - verticalArrangement = Arrangement.spacedBy(8.dp), 226 - ) { 227 - Button( 233 + // Rating target music shortcut 234 + ExpressiveButton( 228 235 onClick = { onRatingTargetClick?.invoke() }, 229 236 enabled = onRatingTargetClick != null, 230 237 modifier = Modifier.fillMaxWidth(), 231 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), 238 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 232 239 ) { 233 240 Text("View DX Rating Calculation Records") 234 241 } ··· 238 245 239 246 // Recent plays header 240 247 item { 241 - Card( 242 - modifier = Modifier.fillMaxWidth(), 243 - shape = RoundedCornerShape(8.dp), 244 - colors = CardDefaults.cardColors(containerColor = CardWhite), 245 - ) { 246 - Text("Recent Plays (${data!!.playLog.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(10.dp).padding(bottom = 4.dp)) 248 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 249 + Text("Recent Plays (${data!!.playLog.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(10.dp).padding(bottom = 4.dp)) 247 250 } 248 251 } 249 252 250 253 // Recent play items (last 50 from dashboard) 251 254 items(recentPlays, key = { "${it.datetime}:${it.name}:${it.difficulty}" }) { play -> 252 - Card( 253 - modifier = Modifier.fillMaxWidth(), 254 - shape = RoundedCornerShape(8.dp), 255 - colors = CardDefaults.cardColors(containerColor = CardWhite), 256 - ) { 255 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 257 256 Row( 258 257 modifier = Modifier 259 258 .fillMaxWidth() ··· 284 283 285 284 // Historical plays (backfilled from before 90 days) 286 285 item { 287 - HorizontalDivider( 286 + ExpressiveHorizontalDivider( 288 287 modifier = Modifier.padding(vertical = 8.dp), 289 - color = MutedText.copy(alpha = 0.2f), 288 + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f), 290 289 ) 291 - Card( 292 - modifier = Modifier.fillMaxWidth(), 293 - shape = RoundedCornerShape(8.dp), 294 - colors = CardDefaults.cardColors(containerColor = CardWhite), 295 - ) { 296 - Text("Historical Plays (${historicalFiltered.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText, modifier = Modifier.padding(10.dp).padding(bottom = 4.dp)) 290 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 291 + Text("Historical Plays (${historicalFiltered.size})", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(10.dp).padding(bottom = 4.dp)) 297 292 } 298 293 } 299 294 if (historicalFiltered.isEmpty()) { ··· 329 324 } 330 325 } else { 331 326 items(historicalFiltered, key = { "h:${it.datetime}:${it.name}:${it.difficulty}" }) { play -> 332 - Card( 333 - modifier = Modifier.fillMaxWidth(), 334 - shape = RoundedCornerShape(8.dp), 335 - colors = CardDefaults.cardColors(containerColor = CardWhite), 336 - ) { 327 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 28.dp) { 337 328 Row( 338 329 modifier = Modifier 339 330 .fillMaxWidth() ··· 371 362 horizontalAlignment = Alignment.CenterHorizontally, 372 363 ) { 373 364 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { 374 - Text(value, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = DarkText) 365 + Text(value, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 375 366 if (showSyncChip) { 376 - Surface(shape = RoundedCornerShape(999.dp), color = DangerRed) { 377 - Icon(Icons.Default.Sync, contentDescription = "Sync historical plays", modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).size(12.dp), tint = Color.White) 367 + ExpressiveBadgeSurface(containerColor = MaterialTheme.colorScheme.error) { 368 + Icon(Icons.Default.Sync, contentDescription = "Sync historical plays", modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).size(12.dp), tint = MaterialTheme.colorScheme.onError) 378 369 } 379 370 } 380 371 } ··· 385 376 modifier = Modifier.height(12.dp), 386 377 contentDescription = null, 387 378 ) 388 - Text(label, fontSize = 11.sp, color = MutedText) 379 + Text(label, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 389 380 } 390 381 } else { 391 - Text(label, fontSize = 11.sp, color = MutedText) 382 + Text(label, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 392 383 } 393 384 } 394 385 } ··· 401 392 result: HistoricalCrawler.Result?, 402 393 onStart: () -> Unit, 403 394 ) { 404 - Card( 405 - modifier = Modifier.fillMaxWidth(), 406 - shape = RoundedCornerShape(8.dp), 407 - colors = CardDefaults.cardColors(containerColor = CardWhite), 408 - ) { 395 + ExpressiveGlassCard(modifier = Modifier.fillMaxWidth(), cornerRadius = 26.dp) { 409 396 Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { 410 - Text("Historical Play Data", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 411 - Text("Backfill your complete play history from before the 90-day window. This crawls your song list across all difficulties.", fontSize = 12.sp, color = MutedText) 397 + Text("Historical Play Data", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 398 + Text("Backfill your complete play history from before the 90-day window. This crawls your song list across all difficulties.", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 412 399 if (backfilling && progress != null) { 413 400 val fraction = progress.current.toFloat() / progress.total.coerceAtLeast(1).toFloat() 414 - LinearProgressIndicator( 401 + ExpressiveLinearProgressIndicator( 415 402 progress = { fraction.coerceIn(0f, 1f) }, 416 403 modifier = Modifier.fillMaxWidth(), 417 - color = PrimaryBlue, 404 + color = MaterialTheme.colorScheme.primary, 418 405 ) 419 - Text(progress.status, fontSize = 12.sp, color = MutedText) 420 - Text("Please keep the app open and on this screen until complete.", fontSize = 11.sp, color = MutedText, fontWeight = FontWeight.Medium) 406 + Text(progress.status, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 407 + Text("Please keep the app open and on this screen until complete.", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Medium) 421 408 } else if (result != null) { 422 - Text("Done! ${result.totalPlays} plays from ${result.totalSongs} songs.", fontSize = 13.sp, color = DarkText, fontWeight = FontWeight.Medium) 409 + Text("Done! ${result.totalPlays} plays from ${result.totalSongs} songs.", fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Medium) 423 410 } else if (error != null) { 424 - Text(error, fontSize = 12.sp, color = DangerRed) 411 + Text(error, fontSize = 12.sp, color = MaterialTheme.colorScheme.error) 425 412 } 426 - Button( 413 + ExpressiveButton( 427 414 onClick = onStart, 428 415 modifier = Modifier.fillMaxWidth(), 429 - shape = RoundedCornerShape(8.dp), 430 - colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), 416 + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 431 417 enabled = !backfilling, 432 418 ) { 433 419 if (backfilling) { 434 - CircularProgressIndicator(modifier = Modifier.size(16.dp), color = CardWhite, strokeWidth = 2.dp) 420 + ExpressiveLoadingIndicator(modifier = Modifier, color = MaterialTheme.colorScheme.onPrimary, size = 16.dp) 435 421 Spacer(Modifier.width(8.dp)) 436 422 } 437 423 Text(if (backfilling) "Backfilling..." else "Backfill Historical Data", fontWeight = FontWeight.SemiBold)
+84 -148
composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/SettingsTab.kt
··· 1 1 package com.derakkuma.ui.tabs 2 2 3 + import com.derakkuma.ui.components.ExpressiveSlider 4 + import com.derakkuma.ui.components.ExpressiveLinearProgressIndicator 5 + import com.derakkuma.ui.components.ExpressiveGlassCard 6 + import com.derakkuma.ui.components.ExpressiveHorizontalDivider 7 + import com.derakkuma.ui.components.ExpressiveTextButton 8 + import com.derakkuma.ui.components.ExpressiveDropdownMenu 9 + import com.derakkuma.ui.components.ExpressiveDropdownMenuItem 10 + import com.derakkuma.ui.components.ExpressiveSnackbar 11 + import com.derakkuma.ui.components.ExpressiveScaffold 12 + import com.derakkuma.ui.components.ExpressiveFloatingActionButton 13 + import com.derakkuma.ui.components.ExpressiveSwitch 3 14 import androidx.compose.foundation.Image 4 - import androidx.compose.foundation.background 5 15 import androidx.compose.foundation.clickable 6 16 import androidx.compose.foundation.layout.* 7 17 import androidx.compose.foundation.rememberScrollState 8 - import androidx.compose.foundation.shape.RoundedCornerShape 9 18 import androidx.compose.foundation.verticalScroll 10 19 import androidx.compose.material.icons.Icons 11 20 import androidx.compose.material.icons.filled.Check ··· 18 27 import androidx.compose.ui.graphics.vector.ImageVector 19 28 import androidx.compose.ui.layout.ContentScale 20 29 import androidx.compose.ui.text.font.FontWeight 21 - import androidx.compose.ui.text.style.TextAlign 22 30 import androidx.compose.ui.unit.dp 23 31 import androidx.compose.ui.unit.sp 24 32 import com.derakkuma.data.* ··· 31 39 import com.derakkuma.ui.images.rememberBundledImage 32 40 import com.derakkuma.ui.theme.* 33 41 import kotlinx.coroutines.launch 34 - import com.derakkuma.ui.components.SplitButtonRow as SharedSplitButtonRow 42 + import com.derakkuma.ui.components.SplitButtonRow 35 43 36 44 @Composable 37 45 fun SettingsTab(client: MaimaiClient, cookie: String, maintenanceMode: Boolean = false) { ··· 62 70 loading = false 63 71 } 64 72 65 - Scaffold( 73 + ExpressiveScaffold( 66 74 contentWindowInsets = WindowInsets(0.dp), 67 75 snackbarHost = { 68 76 if (snackMsg != null) { 69 - Snackbar( 77 + ExpressiveSnackbar( 70 78 modifier = Modifier.padding(12.dp), 71 - shape = RoundedCornerShape(8.dp), 72 79 action = { 73 - TextButton(onClick = { snackMsg = null }) { 74 - Text("OK") 75 - } 80 + ExpressiveTextButton(onClick = { snackMsg = null }) { Text("OK") } 76 81 }, 77 82 ) { Text(snackMsg!!) } 78 83 } 79 84 }, 80 85 floatingActionButton = { 81 86 if (dirty && !saving && !maintenanceMode) { 82 - FloatingActionButton( 87 + ExpressiveFloatingActionButton( 83 88 onClick = { 84 - val opts = options ?: return@FloatingActionButton 89 + val opts = options ?: return@ExpressiveFloatingActionButton 85 90 saving = true 86 91 scope.launch { 87 92 try { ··· 94 99 saving = false 95 100 } 96 101 }, 97 - containerColor = PrimaryBlue, 102 + containerColor = MaterialTheme.colorScheme.primary, 98 103 ) { 99 - Icon(Icons.Default.Check, contentDescription = "Save", tint = CardWhite) 104 + Icon(Icons.Default.Check, contentDescription = "Save", tint = MaterialTheme.colorScheme.onPrimary) 100 105 } 101 106 } 102 107 }, ··· 115 120 val opts = options!! 116 121 117 122 if (saving) { 118 - LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = PrimaryBlue) 123 + ExpressiveLinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.primary) 119 124 } 120 125 121 126 // Friend Registration Skip 122 127 if (friendSkip != null) { 123 - Card( 128 + ExpressiveGlassCard( 124 129 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp), 125 - shape = RoundedCornerShape(8.dp), 126 - colors = CardDefaults.cardColors(containerColor = CardWhite), 130 + cornerRadius = 26.dp, 127 131 ) { 128 132 Row( 129 133 modifier = Modifier.fillMaxWidth().padding(12.dp), ··· 131 135 verticalAlignment = Alignment.CenterVertically, 132 136 ) { 133 137 Column(modifier = Modifier.weight(1f)) { 134 - Text("Friend Registration Skip", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 135 - Text("Skip friend registration after songs", fontSize = 11.sp, color = MutedText) 138 + Text("Friend Registration Skip", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 139 + Text("Skip friend registration after songs", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) 136 140 } 137 141 var skipState by remember { mutableStateOf(friendSkip!!.skip) } 138 - Switch( 142 + ExpressiveSwitch( 139 143 checked = skipState, 140 144 enabled = !maintenanceMode, 141 145 onCheckedChange = { newValue -> ··· 157 161 saving = false 158 162 } 159 163 }, 160 - colors = SwitchDefaults.colors(checkedTrackColor = PrimaryBlue), 161 164 ) 162 165 } 163 166 } ··· 199 202 ) { 200 203 if (fields.isEmpty()) return 201 204 202 - Card( 205 + ExpressiveGlassCard( 203 206 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp), 204 - shape = RoundedCornerShape(8.dp), 205 - colors = CardDefaults.cardColors(containerColor = CardWhite), 207 + cornerRadius = 28.dp, 206 208 ) { 207 209 Column(modifier = Modifier.padding(12.dp)) { 208 - Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = DarkText) 210 + Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) 209 211 Spacer(Modifier.height(8.dp)) 210 212 211 213 fields.forEach { field -> 212 214 OptionRow(field, disabled, onChanged) 213 215 if (field != fields.last()) { 214 - HorizontalDivider( 216 + ExpressiveHorizontalDivider( 215 217 modifier = Modifier.padding(vertical = 4.dp), 216 - color = MutedText.copy(alpha = 0.12f), 218 + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f), 217 219 ) 218 220 } 219 221 } ··· 222 224 } 223 225 224 226 /** Fields that should always be split buttons regardless of option count. */ 225 - private val FORCE_SPLIT_BUTTON = setOf("mirrorMode", "tapDesign", "holdDesign", "slideDesign") 227 + private val FORCE_SPLIT_BUTTON = setOf("mirrorMode", "holdDesign", "slideDesign") 226 228 227 229 /** Icon overrides for specific option indices within a field. */ 228 230 private val OPTION_ICONS: Map<String, Map<Int, ImageVector>> = mapOf( ··· 263 265 264 266 cleaned.name in FORCE_SPLIT_BUTTON -> { 265 267 val icons = OPTION_ICONS[cleaned.name] 266 - if (cleaned.name == "tapDesign") { 267 - TapDesignSplitRow(cleaned, disabled, onChanged) 268 - } else { 269 - SplitButtonRow(cleaned, disabled, onChanged, icons) 270 - } 268 + val label = OPTION_LABELS[cleaned.name] ?: cleaned.name 269 + SplitButtonRow( 270 + label = label, 271 + options = cleaned.options, 272 + selected = cleaned.selected, 273 + disabled = disabled, 274 + icons = icons, 275 + onSelect = { onChanged(cleaned.name, it) }, 276 + ) 271 277 } 272 278 273 279 isSliderField(cleaned) -> SliderRow(cleaned, disabled, onChanged) 274 280 275 - cleaned.options.size in 2..3 -> SplitButtonRow(cleaned, disabled, onChanged) 281 + cleaned.options.size in 2..3 -> { 282 + val label = OPTION_LABELS[cleaned.name] ?: cleaned.name 283 + SplitButtonRow( 284 + label = label, 285 + options = cleaned.options, 286 + selected = cleaned.selected, 287 + disabled = disabled, 288 + onSelect = { onChanged(cleaned.name, it) }, 289 + ) 290 + } 276 291 277 292 else -> DropdownRow(cleaned, disabled, onChanged) 278 293 } ··· 295 310 verticalAlignment = Alignment.CenterVertically, 296 311 horizontalArrangement = Arrangement.SpaceBetween, 297 312 ) { 298 - Text(label, fontSize = 13.sp, fontWeight = FontWeight.Medium, color = DarkText) 313 + Text(label, fontSize = 13.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface) 299 314 Text( 300 315 labels[selectedIdx], 301 316 fontSize = 13.sp, 302 - color = PrimaryBlue, 317 + color = MaterialTheme.colorScheme.primary, 303 318 fontWeight = FontWeight.SemiBold, 304 319 ) 305 320 } 306 - Slider( 321 + ExpressiveSlider( 307 322 value = selectedIdx.toFloat(), 308 323 onValueChange = { raw -> 309 - if (disabled) return@Slider 324 + if (disabled) return@ExpressiveSlider 310 325 val i = (raw + 0.5f).toInt().coerceIn(0, stepCount) 311 326 onChanged(field.name, field.options[i].first) 312 327 }, 313 328 valueRange = 0f..stepCount.toFloat(), 314 329 enabled = !disabled, 315 - colors = SliderDefaults.colors( 316 - thumbColor = PrimaryBlue, 317 - activeTrackColor = PrimaryBlue, 318 - inactiveTrackColor = MutedText.copy(alpha = 0.2f), 319 - ), 320 330 ) 321 331 } 322 - } 323 - 324 - @Composable 325 - private fun SplitButtonRow( 326 - field: OptionField, 327 - disabled: Boolean = false, 328 - onChanged: (fieldName: String, newValue: String) -> Unit, 329 - icons: Map<Int, ImageVector>? = null, 330 - ) { 331 - val label = OPTION_LABELS[field.name] ?: field.name 332 - SharedSplitButtonRow( 333 - label = label, 334 - options = field.options, 335 - selected = field.selected, 336 - disabled = disabled, 337 - icons = icons, 338 - onSelect = { onChanged(field.name, it) }, 339 - ) 340 332 } 341 333 342 334 @OptIn(ExperimentalMaterial3Api::class) ··· 358 350 label, 359 351 fontSize = 13.sp, 360 352 fontWeight = FontWeight.Medium, 361 - color = DarkText, 353 + color = MaterialTheme.colorScheme.onSurface, 362 354 modifier = Modifier.weight(1f), 363 355 ) 364 356 ··· 372 364 Text( 373 365 selectedLabel, 374 366 fontSize = 13.sp, 375 - color = if (disabled) MutedText else PrimaryBlue, 367 + color = if (disabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.primary, 376 368 fontWeight = FontWeight.SemiBold, 377 369 ) 378 370 Icon( 379 371 Icons.Default.ExpandMore, 380 372 contentDescription = null, 381 373 modifier = Modifier.size(18.dp), 382 - tint = if (disabled) MutedText else PrimaryBlue, 374 + tint = if (disabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.primary, 383 375 ) 384 376 } 385 377 386 - DropdownMenu( 378 + ExpressiveDropdownMenu( 387 379 expanded = expanded && !disabled, 388 380 onDismissRequest = { expanded = false }, 389 - containerColor = CardWhite, 390 381 ) { 391 - field.options.forEach { (value, optLabel) -> 392 - DropdownMenuItem( 382 + field.options.forEachIndexed { index, (value, optLabel) -> 383 + val selected = value == field.selected 384 + ExpressiveDropdownMenuItem( 385 + selected = selected, 393 386 text = { 394 387 Text( 395 388 optLabel, 396 389 fontSize = 13.sp, 397 - fontWeight = if (value == field.selected) FontWeight.Bold else FontWeight.Normal, 398 - color = if (value == field.selected) PrimaryBlue else DarkText, 390 + fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal, 399 391 ) 400 392 }, 393 + leadingIcon = if (selected) ({ Icon(Icons.Default.Check, contentDescription = null) }) else null, 394 + index = index, 395 + lastIndex = field.options.lastIndex, 401 396 onClick = { 402 397 onChanged(field.name, value) 403 398 expanded = false ··· 439 434 label, 440 435 fontSize = 13.sp, 441 436 fontWeight = FontWeight.Medium, 442 - color = DarkText, 437 + color = MaterialTheme.colorScheme.onSurface, 443 438 modifier = Modifier.weight(1f), 444 439 ) 445 440 ··· 453 448 Text( 454 449 selectedLabel, 455 450 fontSize = 13.sp, 456 - color = if (disabled) MutedText else PrimaryBlue, 451 + color = if (disabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.primary, 457 452 fontWeight = FontWeight.SemiBold, 458 453 ) 459 454 Icon( 460 455 Icons.Default.ExpandMore, 461 456 contentDescription = null, 462 457 modifier = Modifier.size(18.dp), 463 - tint = if (disabled) MutedText else PrimaryBlue, 458 + tint = if (disabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.primary, 464 459 ) 465 460 } 466 461 467 - DropdownMenu( 462 + ExpressiveDropdownMenu( 468 463 expanded = expanded && !disabled, 469 464 onDismissRequest = { expanded = false }, 470 - containerColor = CardWhite, 471 465 ) { 472 466 // DISPLAY OFF option 473 - DropdownMenuItem( 467 + ExpressiveDropdownMenuItem( 468 + selected = field.selected == "-1", 474 469 text = { 475 470 Text( 476 471 "DISPLAY OFF", 477 472 fontSize = 13.sp, 478 473 fontWeight = if (field.selected == "-1") FontWeight.Bold else FontWeight.Normal, 479 - color = if (field.selected == "-1") PrimaryBlue else DarkText, 480 474 ) 481 475 }, 476 + leadingIcon = if (field.selected == "-1") ({ Icon(Icons.Default.Check, contentDescription = null) }) else null, 477 + index = 0, 478 + lastIndex = field.options.size, 482 479 onClick = { 483 480 onChanged(field.name, "-1") 484 481 expanded = false 485 482 }, 486 483 ) 487 - field.options.forEach { (value, optLabel) -> 488 - DropdownMenuItem( 484 + field.options.forEachIndexed { index, (value, optLabel) -> 485 + val selected = value == field.selected 486 + ExpressiveDropdownMenuItem( 487 + selected = selected, 489 488 text = { 490 489 Text( 491 490 optLabel, 492 491 fontSize = 13.sp, 493 - fontWeight = if (value == field.selected) FontWeight.Bold else FontWeight.Normal, 494 - color = if (value == field.selected) PrimaryBlue else DarkText, 492 + fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal, 495 493 ) 496 494 }, 495 + leadingIcon = if (selected) ({ Icon(Icons.Default.Check, contentDescription = null) }) else null, 496 + index = index + 1, 497 + lastIndex = field.options.size, 497 498 onClick = { 498 499 onChanged(field.name, value) 499 500 expanded = false ··· 513 514 contentDescription = "Judge display preview", 514 515 modifier = Modifier 515 516 .fillMaxWidth() 516 - .clip(RoundedCornerShape(6.dp)), 517 + .clip(MaterialTheme.shapes.large), 517 518 contentScale = ContentScale.FillWidth, 518 519 ) 519 520 } 520 521 } 521 522 } 522 - 523 - /** Tap Design split button — last option shows tapkun icon instead of text. */ 524 - @Composable 525 - private fun TapDesignSplitRow( 526 - field: OptionField, 527 - disabled: Boolean = false, 528 - onChanged: (fieldName: String, newValue: String) -> Unit, 529 - ) { 530 - val label = OPTION_LABELS[field.name] ?: field.name 531 - val tapkunBitmap = remember { mutableStateOf<androidx.compose.ui.graphics.ImageBitmap?>(null) } 532 - LaunchedEffect(Unit) { 533 - tapkunBitmap.value = MaimaiAssets.load("tapkun.png") 534 - } 535 - 536 - Row( 537 - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), 538 - verticalAlignment = Alignment.CenterVertically, 539 - ) { 540 - Text( 541 - label, 542 - fontSize = 13.sp, 543 - fontWeight = FontWeight.Medium, 544 - color = DarkText, 545 - modifier = Modifier.weight(1f), 546 - ) 547 - 548 - Row( 549 - horizontalArrangement = Arrangement.spacedBy(0.dp), 550 - modifier = Modifier.clip(RoundedCornerShape(6.dp)), 551 - ) { 552 - field.options.forEachIndexed { i, (value, optLabel) -> 553 - val selected = value == field.selected 554 - val bgColor = if (selected) PrimaryBlue else MutedText.copy(alpha = 0.1f) 555 - val textColor = if (selected) CardWhite else DarkText 556 - 557 - Box( 558 - modifier = Modifier 559 - .defaultMinSize(minWidth = 44.dp, minHeight = 38.dp) 560 - .background(if (disabled) bgColor.copy(alpha = 0.4f) else bgColor) 561 - .clickable(enabled = !disabled) { onChanged(field.name, value) } 562 - .padding(horizontal = 10.dp, vertical = 6.dp), 563 - contentAlignment = Alignment.Center, 564 - ) { 565 - // Last option (index 4) = Tapkun — show icon if loaded 566 - if (i == 4 && tapkunBitmap.value != null) { 567 - Image( 568 - bitmap = tapkunBitmap.value!!, 569 - contentDescription = "Tapkun", 570 - modifier = Modifier.size(24.dp), 571 - contentScale = ContentScale.Fit, 572 - ) 573 - } else { 574 - Text( 575 - optLabel, 576 - fontSize = 12.sp, 577 - fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium, 578 - color = textColor, 579 - textAlign = TextAlign.Center, 580 - ) 581 - } 582 - } 583 - } 584 - } 585 - } 586 - }
+84 -22
composeApp/src/commonMain/kotlin/com/derakkuma/ui/theme/Theme.kt
··· 1 1 package com.derakkuma.ui.theme 2 2 3 - import androidx.compose.material3.MaterialTheme 3 + import androidx.compose.foundation.shape.RoundedCornerShape 4 + import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 5 + import androidx.compose.material3.MaterialExpressiveTheme 6 + import androidx.compose.material3.MotionScheme 7 + import androidx.compose.material3.Shapes 4 8 import androidx.compose.material3.Typography 5 9 import androidx.compose.material3.lightColorScheme 6 10 import androidx.compose.runtime.Composable 7 11 import androidx.compose.ui.graphics.Color 12 + import androidx.compose.ui.text.TextStyle 8 13 import androidx.compose.ui.text.font.FontFamily 9 14 import androidx.compose.ui.text.font.FontWeight 15 + import androidx.compose.ui.unit.dp 16 + import androidx.compose.ui.unit.sp 10 17 import derakkuma.composeapp.generated.resources.Res 11 18 import derakkuma.composeapp.generated.resources.segamaru_db 12 19 import org.jetbrains.compose.resources.ExperimentalResourceApi 13 20 import org.jetbrains.compose.resources.Font 14 21 15 22 val MaimaiBlue = Color(0xFF51BCF3) 16 - val CardWhite = Color(0xE0FFFFFF) 23 + val CardWhite = Color(0xF2FFFFFF) 17 24 val DarkText = Color(0xFF111827) 18 25 val MutedText = Color(0xFF6B7280) 19 26 val AccentPink = Color(0xFFBE185D) 20 - val PrimaryBlue = Color(0xFF2563EB) 27 + val PrimaryBlue = Color(0xFF1769F5) 21 28 val DangerRed = Color(0xFFDC2626) 22 29 30 + // Material 3 expressive palette: saturated, game-like colors with soft candy surfaces. 31 + val ExpressiveSky = Color(0xFF43BDF4) 32 + val ExpressiveOcean = Color(0xFF1769F5) 33 + val ExpressiveBubblegum = Color(0xFFFF5FA2) 34 + val ExpressiveLemon = Color(0xFFFFD84D) 35 + val ExpressiveLime = Color(0xFF7CD82E) 36 + val ExpressiveGrape = Color(0xFF865DFF) 37 + val ExpressivePeach = Color(0xFFFF8D5C) 38 + val ExpressiveMint = Color(0xFFBFF7E2) 39 + val ExpressiveSurface = Color(0xFFF8FCFF) 40 + val ExpressiveSurfaceVariant = Color(0xFFE7F4FF) 41 + val ExpressiveOutline = Color(0x332563EB) 42 + 23 43 // maimai difficulty colors 24 44 val DiffBasic = Color(0xFF22BB5B) 25 45 val DiffAdvanced = Color(0xFFF9A825) ··· 27 47 val DiffMaster = Color(0xFFAB47BC) 28 48 val DiffRemaster = Color(0xFFE0C3FF) 29 49 50 + // Ranking medal colors 51 + val MedalGold = Color(0xFFFFD700) 52 + val MedalSilver = Color(0xFFC0C0C0) 53 + val MedalBronze = Color(0xFFCD7F32) 54 + 30 55 fun difficultyColor(difficulty: String): Color = when (difficulty.lowercase()) { 31 56 "basic" -> DiffBasic 32 57 "advanced" -> DiffAdvanced ··· 37 62 } 38 63 39 64 private val colorScheme = lightColorScheme( 40 - primary = PrimaryBlue, 41 - background = MaimaiBlue, 42 - surface = CardWhite, 65 + primary = ExpressiveOcean, 66 + onPrimary = Color.White, 67 + primaryContainer = Color(0xFFD7E7FF), 68 + onPrimaryContainer = Color(0xFF06285E), 69 + secondary = ExpressiveBubblegum, 70 + onSecondary = Color.White, 71 + secondaryContainer = Color(0xFFFFD8E6), 72 + onSecondaryContainer = Color(0xFF5A0C2C), 73 + tertiary = ExpressiveGrape, 74 + onTertiary = Color.White, 75 + tertiaryContainer = Color(0xFFE9DDFF), 76 + onTertiaryContainer = Color(0xFF27105C), 77 + background = ExpressiveSky, 78 + onBackground = Color.White, 79 + surface = ExpressiveSurface, 43 80 onSurface = DarkText, 81 + surfaceVariant = ExpressiveSurfaceVariant, 82 + onSurfaceVariant = MutedText, 83 + outline = ExpressiveOutline, 44 84 error = DangerRed, 85 + errorContainer = Color(0xFFFFDAD6), 86 + onErrorContainer = Color(0xFF410002), 87 + ) 88 + 89 + val ExpressiveShapes = Shapes( 90 + extraSmall = RoundedCornerShape(12.dp), 91 + small = RoundedCornerShape(16.dp), 92 + medium = RoundedCornerShape(24.dp), 93 + large = RoundedCornerShape(32.dp), 94 + extraLarge = RoundedCornerShape(40.dp), 45 95 ) 46 96 47 97 @OptIn(ExperimentalResourceApi::class) ··· 56 106 @Composable 57 107 fun SegaMaruTypography() = Typography().run { 58 108 val fontFamily = SegaMaruFontFamily() 109 + fun TextStyle.withDerakkuma( 110 + weight: FontWeight = FontWeight.Normal, 111 + tracking: Float = 0f, 112 + ) = copy( 113 + fontFamily = fontFamily, 114 + fontWeight = weight, 115 + letterSpacing = tracking.sp, 116 + ) 117 + 59 118 copy( 60 - displayLarge = displayLarge.copy(fontFamily = fontFamily), 61 - displayMedium = displayMedium.copy(fontFamily = fontFamily), 62 - displaySmall = displaySmall.copy(fontFamily = fontFamily), 63 - headlineLarge = headlineLarge.copy(fontFamily = fontFamily), 64 - headlineMedium = headlineMedium.copy(fontFamily = fontFamily), 65 - headlineSmall = headlineSmall.copy(fontFamily = fontFamily), 66 - titleLarge = titleLarge.copy(fontFamily = fontFamily), 67 - titleMedium = titleMedium.copy(fontFamily = fontFamily), 68 - titleSmall = titleSmall.copy(fontFamily = fontFamily), 69 - bodyLarge = bodyLarge.copy(fontFamily = fontFamily), 70 - bodyMedium = bodyMedium.copy(fontFamily = fontFamily), 71 - bodySmall = bodySmall.copy(fontFamily = fontFamily), 72 - labelLarge = labelLarge.copy(fontFamily = fontFamily), 73 - labelMedium = labelMedium.copy(fontFamily = fontFamily), 74 - labelSmall = labelSmall.copy(fontFamily = fontFamily), 119 + displayLarge = displayLarge.withDerakkuma(FontWeight.ExtraBold, -0.8f), 120 + displayMedium = displayMedium.withDerakkuma(FontWeight.ExtraBold, -0.6f), 121 + displaySmall = displaySmall.withDerakkuma(FontWeight.ExtraBold, -0.4f), 122 + headlineLarge = headlineLarge.withDerakkuma(FontWeight.ExtraBold, -0.3f), 123 + headlineMedium = headlineMedium.withDerakkuma(FontWeight.ExtraBold, -0.2f), 124 + headlineSmall = headlineSmall.withDerakkuma(FontWeight.Bold, -0.1f), 125 + titleLarge = titleLarge.withDerakkuma(FontWeight.ExtraBold, -0.1f), 126 + titleMedium = titleMedium.withDerakkuma(FontWeight.Bold), 127 + titleSmall = titleSmall.withDerakkuma(FontWeight.SemiBold), 128 + bodyLarge = bodyLarge.withDerakkuma(FontWeight.Normal), 129 + bodyMedium = bodyMedium.withDerakkuma(FontWeight.Normal), 130 + bodySmall = bodySmall.withDerakkuma(FontWeight.Normal), 131 + labelLarge = labelLarge.withDerakkuma(FontWeight.ExtraBold, 0.2f), 132 + labelMedium = labelMedium.withDerakkuma(FontWeight.Bold, 0.2f), 133 + labelSmall = labelSmall.withDerakkuma(FontWeight.SemiBold, 0.2f), 75 134 ) 76 135 } 77 136 137 + @OptIn(ExperimentalMaterial3ExpressiveApi::class) 78 138 @Composable 79 139 fun DerakkumaTheme(content: @Composable () -> Unit) { 80 - MaterialTheme( 140 + MaterialExpressiveTheme( 81 141 colorScheme = colorScheme, 142 + motionScheme = MotionScheme.expressive(), 82 143 typography = SegaMaruTypography(), 144 + shapes = ExpressiveShapes, 83 145 content = content, 84 146 ) 85 147 }
+4 -4
composeApp/src/commonMain/kotlin/com/derakkuma/ui/util/CopyButton.kt
··· 3 3 import androidx.compose.material.icons.Icons 4 4 import androidx.compose.material.icons.filled.ContentCopy 5 5 import androidx.compose.material3.Icon 6 - import androidx.compose.material3.IconButton 6 + import androidx.compose.material3.MaterialTheme 7 7 import androidx.compose.runtime.Composable 8 8 import androidx.compose.runtime.rememberCoroutineScope 9 9 import androidx.compose.ui.Modifier 10 10 import androidx.compose.ui.platform.LocalClipboard 11 - import com.derakkuma.ui.theme.PrimaryBlue 11 + import com.derakkuma.ui.components.ExpressiveFilledTonalIconButton 12 12 import kotlinx.coroutines.launch 13 13 14 14 @Composable ··· 16 16 val clipboard = LocalClipboard.current 17 17 val scope = rememberCoroutineScope() 18 18 19 - IconButton( 19 + ExpressiveFilledTonalIconButton( 20 20 onClick = { 21 21 scope.launch { 22 22 text.copyToClipboard(clipboard) ··· 27 27 Icon( 28 28 imageVector = Icons.Default.ContentCopy, 29 29 contentDescription = "Copy", 30 - tint = PrimaryBlue, 30 + tint = MaterialTheme.colorScheme.primary, 31 31 ) 32 32 } 33 33 }
+5 -6
composeApp/src/iosMain/kotlin/com/derrakuma/appicon/AppIcon.ios.kt
··· 19 19 @Composable 20 20 actual fun platformMonochromeAppIconColors(): Pair<Color, Color> = Color(0xFFEADDFF) to Color(0xFF4F378B) 21 21 22 - actual suspend fun loadAppIconPreviewAsset(path: String): ImageBitmap? = 23 - runCatching { 24 - val name = path.substringAfterLast('/').substringBeforeLast(".") 25 - val image = UIImage.imageNamed(name) ?: return@runCatching null 26 - UIImagePNGRepresentation(image)?.toByteArray()?.decodeToImageBitmap() 27 - }.getOrNull() 22 + actual suspend fun loadAppIconPreviewAsset(path: String): ImageBitmap? = runCatching { 23 + val name = path.substringAfterLast('/').substringBeforeLast(".") 24 + val image = UIImage.imageNamed(name) ?: return@runCatching null 25 + UIImagePNGRepresentation(image)?.toByteArray()?.decodeToImageBitmap() 26 + }.getOrNull() 28 27 29 28 actual fun setAppIcon(option: AppIconOption): Boolean { 30 29 setAlternateAppIconName(option.iosName)
+1
gradle.properties
··· 2 2 kotlin.code.style=official 3 3 android.useAndroidX=true 4 4 android.nonTransitiveRClass=true 5 + org.gradle.configuration-cache=true
+6 -1
gradle/libs.versions.toml
··· 1 1 [versions] 2 2 kotlin = "2.3.20" 3 3 compose-multiplatform = "1.11.0" 4 - agp = "8.9.3" 4 + agp = "9.1.1" 5 5 ktor = "3.5.0" 6 6 kotlinx-serialization = "1.11.0" 7 7 kotlinx-coroutines = "1.10.2" 8 8 kotlinx-datetime = "0.7.1" 9 9 androidx-activity = "1.10.1" 10 10 androidx-lifecycle = "2.9.0" 11 + androidx-browser = "1.8.0" 12 + androidx-core = "1.17.0" 11 13 navigation-compose = "2.9.0" 12 14 datastore = "1.1.7" 13 15 sqldelight = "2.1.0" ··· 34 36 kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } 35 37 36 38 androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } 39 + androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" } 40 + androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } 37 41 androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } 38 42 androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } 39 43 navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" } ··· 65 69 compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } 66 70 compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 67 71 android-application = { id = "com.android.application", version.ref = "agp" } 72 + android-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } 68 73 sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
+1 -1
gradle/wrapper/gradle-wrapper.properties
··· 1 1 distributionBase=GRADLE_USER_HOME 2 2 distributionPath=wrapper/dists 3 - distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 3 + distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip 4 4 networkTimeout=10000 5 5 retries=0 6 6 retryBackOffMs=500
+1
settings.gradle.kts
··· 17 17 } 18 18 19 19 include(":composeApp") 20 + include(":androidApp") 20 21 21 22 includeBuild("third_party/Compose-HeatMap") { 22 23 dependencySubstitution {