From 79b4f12a14f9d36b76dc223f675ce4ff16b890ea Mon Sep 17 00:00:00 2001 From: day_3 <3229208173@qq.com> Date: Wed, 7 Oct 2026 17:23:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=BB=84=E5=90=88=E6=8E=B7?= =?UTF-8?q?=E9=AA=B0=E5=8A=9F=E8=83=BD=E5=92=8C=20Android=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +- androidApp/build.gradle.kts | 42 ++++ androidApp/src/main/AndroidManifest.xml | 17 ++ .../git/diceroller/android/MainActivity.kt | 30 +++ build.gradle.kts | 4 +- gradle.properties | 4 +- gradle/libs.versions.toml | 7 +- gradle/wrapper/gradle-wrapper.properties | 4 +- settings.gradle.kts | 3 +- shared/build.gradle.kts | 19 +- .../kotlin/cn/itouc/git/diceroller/App.kt | 225 ++++++++++++++++-- 11 files changed, 328 insertions(+), 39 deletions(-) create mode 100644 androidApp/build.gradle.kts create mode 100644 androidApp/src/main/AndroidManifest.xml create mode 100644 androidApp/src/main/kotlin/cn/itouc/git/diceroller/android/MainActivity.kt diff --git a/README.md b/README.md index 745446e..9376968 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,17 @@ - [x] 50分 至少可以 roll D6 任意次 -- [ ] 30分 可以实现 roll D4,D6,D8,D12(或者更多)任意切换 +- [x] 30分 可以实现 roll D4,D6,D8,D12(或者更多)任意切换 -- [ ] 20分 可以实现 roll n D6(指同时掷出 n 个六面骰并将点数加和) +- [x] 20分 可以实现 roll n D6(指同时掷出 n 个六面骰并将点数加和) -- [ ] 20分 可以实现 roll n D m(指同时掷出 n 个 m 面骰并将点数加和) +- [x] 20分 可以实现 roll n D m(指同时掷出 n 个 m 面骰并将点数加和) -- [ ] 20分 可以实现 roll $n_1$ D $m_1$ , $n_2$ D $m_2$ ...... 任意组合,最后点数取和 +- [x] 20分 可以实现 roll $n_1$ D $m_1$ , $n_2$ D $m_2$ ...... 任意组合,最后点数取和 ### 额外 共35分 -- [ ] 5分 支持 Android 平台的编译与安装 +- [x] 5分 支持 Android 平台的编译与安装 - [ ] 5分 支持浏览器 WASM 编译 @@ -46,7 +46,7 @@ - [ ] 5分 有应用图标 -- [ ] 5分 好看,达到我的审美 +- [?] 5分 好看,达到我的审美 - [ ] 5分 根据实现方法,代码格式化等的特殊得分 diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts new file mode 100644 index 0000000..861404f --- /dev/null +++ b/androidApp/build.gradle.kts @@ -0,0 +1,42 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) +} + +android { + namespace = "cn.itouc.git.diceroller.android" + compileSdk = 37 + + defaultConfig { + applicationId = "cn.itouc.git.diceroller" + minSdk = 24 + targetSdk = 37 + versionCode = 1 + versionName = "1.0" + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +dependencies { + implementation(project(":shared")) + implementation(libs.androidx.activity.compose) + implementation(libs.compose.runtime) + implementation(libs.compose.foundation) +} diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2f9568b --- /dev/null +++ b/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/androidApp/src/main/kotlin/cn/itouc/git/diceroller/android/MainActivity.kt b/androidApp/src/main/kotlin/cn/itouc/git/diceroller/android/MainActivity.kt new file mode 100644 index 0000000..ee3f1cc --- /dev/null +++ b/androidApp/src/main/kotlin/cn/itouc/git/diceroller/android/MainActivity.kt @@ -0,0 +1,30 @@ +package cn.itouc.git.diceroller.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.ui.Modifier +import cn.itouc.git.diceroller.App + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + Box( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .imePadding() + ) { + App() + } + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 627d2c9..5ecb6db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,10 @@ plugins { + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidKmpLibrary) apply false // this is necessary to avoid the plugins to be loaded multiple times // in each subproject's classloader alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.kotlinJvm) apply false alias(libs.plugins.kotlinMultiplatform) apply false -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 41d680b..7213fd4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,6 @@ kotlin.daemon.jvmargs=-Xmx3072M #Gradle org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8 org.gradle.configuration-cache=true -org.gradle.caching=true \ No newline at end of file +org.gradle.caching=true +android.useAndroidX=true +android.overridePathCheck=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66e2535..874ca4f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,6 @@ [versions] +agp = "9.4.0" +androidx-activity = "1.10.1" androidx-lifecycle = "2.11.0" composeMultiplatform = "1.12.0" junit = "4.13.2" @@ -7,6 +9,7 @@ kotlinx-coroutines = "1.11.0" material3 = "1.12.0-alpha03" [libraries] +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { module = "junit:junit", version.ref = "junit" } @@ -21,7 +24,9 @@ compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-previ kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } [plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 56eb7cc..760c459 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,10 +1,10 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f +distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 632e069..528e772 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,4 +34,5 @@ plugins { } include(":desktopApp") -include(":shared") \ No newline at end of file +include(":shared") +include(":androidApp") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 0d6dd37..48b0a01 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,4 +1,7 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { + alias(libs.plugins.androidKmpLibrary) alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) @@ -7,6 +10,20 @@ plugins { kotlin { jvm() + android { + namespace = "cn.itouc.git.diceroller.shared" + compileSdk = 37 + minSdk = 24 + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + + androidResources { + enable = true + } + } + sourceSets { commonMain.dependencies { implementation(libs.compose.runtime) @@ -22,4 +39,4 @@ kotlin { implementation(libs.kotlin.test) } } -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/cn/itouc/git/diceroller/App.kt b/shared/src/commonMain/kotlin/cn/itouc/git/diceroller/App.kt index b596ffd..25c13f2 100644 --- a/shared/src/commonMain/kotlin/cn/itouc/git/diceroller/App.kt +++ b/shared/src/commonMain/kotlin/cn/itouc/git/diceroller/App.kt @@ -1,56 +1,229 @@ package cn.itouc.git.diceroller -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import org.jetbrains.compose.resources.painterResource - -import diceroller.shared.generated.resources.Res -import diceroller.shared.generated.resources.compose_multiplatform +import androidx.compose.ui.unit.dp import kotlin.random.Random +data class DiceGroup( + val count: Int, + val sides: Int +) + +data class DiceGroupResult( + val group: DiceGroup, + val points: List +) { + val total: Long + get() = points.sumOf { it.toLong() } +} + @Composable -@Preview fun App() { MaterialTheme { - var showContent by remember { mutableStateOf(false) } - var randomInt by remember { mutableStateOf(Random.nextInt()) } + var sidesInput by remember { mutableStateOf("6") } + var countInput by remember { mutableStateOf("1") } + + var groups by remember { + mutableStateOf(emptyList()) + } + + var results by remember { + mutableStateOf(emptyList()) + } + + val diceSides = sidesInput.toIntOrNull() + val diceCount = countInput.toIntOrNull() + + val validSides = diceSides != null && + diceSides >= 1 && + diceSides < Int.MAX_VALUE + + val validCount = diceCount != null && + diceCount in 1..1000 + + val totalDiceCount = groups.sumOf { it.count } + + val withinLimit = diceCount != null && + diceCount in 1..1000 && + totalDiceCount + diceCount <= 1000 Column( modifier = Modifier - .background(MaterialTheme.colorScheme.primaryContainer) - .safeContentPadding() - .fillMaxSize(), + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) ) { + Text( + text = "组合掷骰子", + style = MaterialTheme.typography.headlineMedium + ) + + OutlinedTextField( + value = sidesInput, + onValueChange = { sidesInput = it }, + label = { Text("骰子面数") }, + singleLine = true, + isError = !validSides + ) + + if (!validSides) { + Text( + text = "面数请输入 1~2147483646 的整数", + color = MaterialTheme.colorScheme.error + ) + } + + OutlinedTextField( + value = countInput, + onValueChange = { countInput = it }, + label = { Text("骰子数量") }, + singleLine = true, + isError = !validCount + ) + + if (!validCount) { + Text( + text = "数量请输入 1~1000 的整数", + color = MaterialTheme.colorScheme.error + ) + } else if (!withinLimit) { + Text( + text = "添加后将超过总数限制,最多允许 1000 个骰子", + color = MaterialTheme.colorScheme.error + ) + } + Button( + enabled = validSides && validCount && withinLimit, onClick = { - showContent = !showContent - if (showContent) randomInt = Random.nextInt() + val sides = diceSides + val count = diceCount + + if ( + sides != null && + sides >= 1 && + sides < Int.MAX_VALUE && + count != null && + count in 1..1000 && + groups.sumOf { it.count } + count <= 1000 + ) { + groups = groups + DiceGroup( + count = count, + sides = sides + ) + + results = emptyList() + } } ) { - Text("Click me!") + Text("添加骰子组") } - AnimatedVisibility(showContent) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image(painterResource(Res.drawable.compose_multiplatform), null) - Text("a random int: $randomInt") + + Text( + text = "当前组合", + style = MaterialTheme.typography.titleLarge + ) + + if (groups.isEmpty()) { + Text("还没有骰子组,请先添加") + } else { + groups.forEachIndexed { index, group -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "第 ${index + 1} 组:${group.count}D${group.sides}", + modifier = Modifier.weight(1f) + ) + + TextButton( + onClick = { + groups = groups.filterIndexed { groupIndex, _ -> + groupIndex != index + } + results = emptyList() + } + ) { + Text("删除") + } + } } } + + Text("共 ${groups.size} 组,$totalDiceCount / 1000 个骰子") + + Button( + enabled = groups.isNotEmpty(), + onClick = { + results = groups.map { group -> + DiceGroupResult( + group = group, + points = List(group.count) { + Random.nextInt(1, group.sides + 1) + } + ) + } + } + ) { + Text("全部掷骰") + } + + if (results.isNotEmpty()) { + Text( + text = "掷骰结果", + style = MaterialTheme.typography.titleLarge + ) + + results.forEachIndexed { index, result -> + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = "第 ${index + 1} 组:" + + "${result.group.count}D${result.group.sides}", + style = MaterialTheme.typography.titleMedium + ) + + val pointsText = result.points + .take(20) + .joinToString(" + ") + + val remainingHint = + if (result.points.size > 20) { + " + …" + } else { + "" + } + + Text("点数:$pointsText$remainingHint") + Text("小计:${result.total}") + } + } + + val grandTotal = results.sumOf { it.total } + + Text( + text = "总和:$grandTotal", + style = MaterialTheme.typography.headlineMedium + ) + } } } } \ No newline at end of file