feat: add installable Lingji Android shell
This commit is contained in:
+122
-19
@@ -1,26 +1,110 @@
|
||||
import groovy.json.JsonSlurper
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.kapt)
|
||||
alias(libs.plugins.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.opcapp.flash"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "net.opcapp.flash"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += mapOf("room.schemaLocation" to "$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = false
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
kotlin.srcDir("src/main/java")
|
||||
}
|
||||
test {
|
||||
kotlin.srcDir("src/test/java")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions.jvmTarget = "17"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val composeBom = platform(libs.androidx.compose.bom)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(composeBom)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
kapt(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.hilt.android)
|
||||
kapt(libs.hilt.compiler)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
|
||||
androidTestImplementation(composeBom)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnit()
|
||||
testLogging {
|
||||
events("failed", "skipped")
|
||||
@@ -30,25 +114,44 @@ tasks.test {
|
||||
|
||||
tasks.register("verifyDependencyLicenses") {
|
||||
group = "verification"
|
||||
description = "Ensures every resolved JVM runtime/test artifact has a reviewed license entry."
|
||||
description = "Ensures every resolved debug runtime/test artifact has a reviewed license entry."
|
||||
|
||||
doLast {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val manifest = JsonSlurper().parse(rootProject.file("config/dependency-licenses.json")) as Map<String, Any>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val components = manifest.getValue("components") as List<Map<String, Any>>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val groupPolicies = manifest.getValue("groupPolicies") as List<Map<String, Any>>
|
||||
val reviewedCoordinates = components.mapNotNull { it["coordinates"] as String? }.toSet()
|
||||
val resolvedCoordinates = configurations.getByName("testRuntimeClasspath")
|
||||
.resolvedConfiguration
|
||||
.resolvedArtifacts
|
||||
.map { artifact ->
|
||||
val id = artifact.moduleVersion.id
|
||||
"${id.group}:${id.name}:${id.version}"
|
||||
val resolvedCoordinates = listOf("debugRuntimeClasspath", "debugUnitTestRuntimeClasspath")
|
||||
.flatMap { configurationName ->
|
||||
configurations.getByName(configurationName)
|
||||
.resolvedConfiguration
|
||||
.resolvedArtifacts
|
||||
.mapNotNull { artifact ->
|
||||
if (artifact.id.componentIdentifier !is org.gradle.api.artifacts.component.ModuleComponentIdentifier) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val id = artifact.moduleVersion.id
|
||||
"${id.group}:${id.name}:${id.version}"
|
||||
}
|
||||
}
|
||||
.toSet()
|
||||
val missing = resolvedCoordinates - reviewedCoordinates
|
||||
val missing = resolvedCoordinates.filterNot { coordinates ->
|
||||
if (coordinates in reviewedCoordinates) return@filterNot true
|
||||
val group = coordinates.substringBefore(":")
|
||||
groupPolicies.any { policy ->
|
||||
val prefix = policy.getValue("groupPrefix") as String
|
||||
group == prefix || group.startsWith("$prefix.")
|
||||
}
|
||||
}
|
||||
check(missing.isEmpty()) {
|
||||
"Dependencies missing reviewed license entries: ${missing.sorted().joinToString()}"
|
||||
}
|
||||
logger.lifecycle(
|
||||
"Dependency license coverage passed for ${resolvedCoordinates.size} artifacts " +
|
||||
"with ${groupPolicies.size} reviewed group policies.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
# Project-specific R8 rules will be added when release minification is enabled.
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.opcapp.flash.app
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MainActivityTest {
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<MainActivity>()
|
||||
|
||||
@Test
|
||||
fun domainFixtureCanBeVerifiedFromHome() {
|
||||
composeRule.onNodeWithText("灵机").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("验证起卦核心").performClick()
|
||||
composeRule.onNodeWithText("复 24 → 坤 2").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("初爻动").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:name=".app.LingjiApplication"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Lingji"
|
||||
android:usesCleartextTraffic="false">
|
||||
<activity
|
||||
android:name=".app.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.opcapp.flash.app
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import net.opcapp.flash.feature.home.DevelopmentHomeScreen
|
||||
import net.opcapp.flash.feature.result.DomainVerificationScreen
|
||||
|
||||
private object Destination {
|
||||
const val Home = "home"
|
||||
const val DomainResult = "domain-result"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LingjiApp() {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Destination.Home,
|
||||
) {
|
||||
composable(Destination.Home) {
|
||||
DevelopmentHomeScreen(
|
||||
onVerifyDomain = { navController.navigate(Destination.DomainResult) },
|
||||
)
|
||||
}
|
||||
composable(Destination.DomainResult) {
|
||||
DomainVerificationScreen(
|
||||
onBack = navController::popBackStack,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.opcapp.flash.app
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class LingjiApplication : Application()
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.opcapp.flash.app
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import net.opcapp.flash.core.designsystem.LingjiTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
LingjiTheme {
|
||||
LingjiApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.opcapp.flash.core.designsystem
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
private val Paper = Color(0xFFF5F0E6)
|
||||
private val RaisedPaper = Color(0xFFFFFBF3)
|
||||
private val Ink = Color(0xFF26231F)
|
||||
private val MutedInk = Color(0xFF665F55)
|
||||
private val Cinnabar = Color(0xFFA33A2B)
|
||||
private val DarkPaper = Color(0xFF1E1B18)
|
||||
private val DarkRaisedPaper = Color(0xFF292520)
|
||||
private val PaleInk = Color(0xFFEAE2D5)
|
||||
private val PaleCinnabar = Color(0xFFFFB4A5)
|
||||
|
||||
private val LightColors = lightColorScheme(
|
||||
primary = Cinnabar,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Color(0xFFF4DAD3),
|
||||
onPrimaryContainer = Color(0xFF3D0902),
|
||||
background = Paper,
|
||||
onBackground = Ink,
|
||||
surface = RaisedPaper,
|
||||
onSurface = Ink,
|
||||
surfaceVariant = Color(0xFFE8E0D3),
|
||||
onSurfaceVariant = MutedInk,
|
||||
outline = Color(0xFF81786C),
|
||||
)
|
||||
|
||||
private val DarkColors = darkColorScheme(
|
||||
primary = PaleCinnabar,
|
||||
onPrimary = Color(0xFF5F160C),
|
||||
primaryContainer = Color(0xFF7F261A),
|
||||
onPrimaryContainer = Color(0xFFFFDAD2),
|
||||
background = DarkPaper,
|
||||
onBackground = PaleInk,
|
||||
surface = DarkRaisedPaper,
|
||||
onSurface = PaleInk,
|
||||
surfaceVariant = Color(0xFF4C453D),
|
||||
onSurfaceVariant = Color(0xFFD1C6B8),
|
||||
outline = Color(0xFF9C9184),
|
||||
)
|
||||
|
||||
private val LingjiTypography = Typography().let { defaults ->
|
||||
defaults.copy(
|
||||
displaySmall = defaults.displaySmall.copy(
|
||||
fontFamily = FontFamily.Serif,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
headlineMedium = defaults.headlineMedium.copy(
|
||||
fontFamily = FontFamily.Serif,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
titleLarge = defaults.titleLarge.copy(
|
||||
fontFamily = FontFamily.Serif,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
bodyLarge = defaults.bodyLarge.copy(fontFamily = FontFamily.SansSerif),
|
||||
bodyMedium = defaults.bodyMedium.copy(fontFamily = FontFamily.SansSerif),
|
||||
labelLarge = defaults.labelLarge.copy(
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LingjiTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = if (darkTheme) DarkColors else LightColors,
|
||||
typography = LingjiTypography,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package brainwave.core.model
|
||||
package net.opcapp.flash.core.model
|
||||
|
||||
data class HexagramContent(
|
||||
val kingWenNumber: Int,
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package brainwave.data.content
|
||||
package net.opcapp.flash.data.content
|
||||
|
||||
import brainwave.core.model.HexagramContent
|
||||
import net.opcapp.flash.core.model.HexagramContent
|
||||
|
||||
interface HexagramContentRepository {
|
||||
val contentVersion: String
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package brainwave.domain.casting
|
||||
package net.opcapp.flash.domain.casting
|
||||
|
||||
data class CastComputation internal constructor(
|
||||
val roundsBottomUp: List<CastRound>,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package brainwave.domain.casting
|
||||
package net.opcapp.flash.domain.casting
|
||||
|
||||
data class CastRecordDto(
|
||||
val schemaVersion: Int,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package brainwave.domain.casting
|
||||
package net.opcapp.flash.domain.casting
|
||||
|
||||
enum class CoinSide(val score: Int) {
|
||||
CHARACTER(2),
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package brainwave.domain.casting
|
||||
package net.opcapp.flash.domain.casting
|
||||
|
||||
object HexagramCatalog {
|
||||
private enum class Trigram(val patternBottomUp: String) {
|
||||
@@ -0,0 +1,131 @@
|
||||
package net.opcapp.flash.feature.home
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.opcapp.flash.R
|
||||
|
||||
@Composable
|
||||
fun DevelopmentHomeScreen(
|
||||
onVerifyDomain: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.development_build),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.height(28.dp))
|
||||
HexagramSeal()
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.home_tagline),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.core_check_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.core_check_body),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Button(
|
||||
onClick = onVerifyDomain,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
contentPadding = PaddingValues(horizontal = 20.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringResource(R.string.verify_casting_core))
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.local_data_notice),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HexagramSeal() {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
modifier = Modifier.height(72.dp),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.padding(horizontal = 22.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.seal_character),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package net.opcapp.flash.feature.result
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.opcapp.flash.R
|
||||
import net.opcapp.flash.domain.casting.CastEngine
|
||||
import net.opcapp.flash.domain.casting.CastRound
|
||||
import net.opcapp.flash.domain.casting.LineValue
|
||||
import net.opcapp.flash.domain.casting.Polarity
|
||||
|
||||
@Composable
|
||||
fun DomainVerificationScreen(
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val result = remember {
|
||||
CastEngine.cast(
|
||||
listOf(
|
||||
CastRound.fromLineValue(LineValue.OLD_YANG),
|
||||
CastRound.fromLineValue(LineValue.YOUNG_YIN),
|
||||
CastRound.fromLineValue(LineValue.YOUNG_YIN),
|
||||
CastRound.fromLineValue(LineValue.YOUNG_YIN),
|
||||
CastRound.fromLineValue(LineValue.YOUNG_YIN),
|
||||
CastRound.fromLineValue(LineValue.YOUNG_YIN),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier.height(48.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.back))
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.core_check_passed),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.fixture_result,
|
||||
result.primaryHexagramId.value,
|
||||
result.transformedHexagramId.value,
|
||||
),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.moving_line_result),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Spacer(Modifier.height(36.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
HexagramDiagram(
|
||||
linesBottomUp = result.primaryPatternBottomUp.linesBottomUp,
|
||||
description = stringResource(R.string.primary_hexagram_description),
|
||||
)
|
||||
HexagramDiagram(
|
||||
linesBottomUp = result.transformedPatternBottomUp.linesBottomUp,
|
||||
description = stringResource(R.string.transformed_hexagram_description),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(36.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.fixture_explanation),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HexagramDiagram(
|
||||
linesBottomUp: List<Polarity>,
|
||||
description: String,
|
||||
) {
|
||||
val ink = MaterialTheme.colorScheme.onBackground
|
||||
val lineWidth = with(LocalDensity.current) { 58.dp.toPx() }
|
||||
val gap = with(LocalDensity.current) { 12.dp.toPx() }
|
||||
val strokeWidth = with(LocalDensity.current) { 5.dp.toPx() }
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.size(width = 88.dp, height = 136.dp)
|
||||
.semantics { contentDescription = description },
|
||||
) {
|
||||
val verticalStep = size.height / 6f
|
||||
linesBottomUp.forEachIndexed { index, polarity ->
|
||||
val y = size.height - verticalStep * (index + 0.5f)
|
||||
val left = (size.width - lineWidth) / 2f
|
||||
val right = left + lineWidth
|
||||
if (polarity == Polarity.YANG) {
|
||||
drawLine(
|
||||
color = ink,
|
||||
start = Offset(left, y),
|
||||
end = Offset(right, y),
|
||||
strokeWidth = strokeWidth,
|
||||
cap = StrokeCap.Square,
|
||||
)
|
||||
} else {
|
||||
val center = size.width / 2f
|
||||
drawLine(
|
||||
color = ink,
|
||||
start = Offset(left, y),
|
||||
end = Offset(center - gap / 2f, y),
|
||||
strokeWidth = strokeWidth,
|
||||
cap = StrokeCap.Square,
|
||||
)
|
||||
drawLine(
|
||||
color = ink,
|
||||
start = Offset(center + gap / 2f, y),
|
||||
end = Offset(right, y),
|
||||
strokeWidth = strokeWidth,
|
||||
cap = StrokeCap.Square,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Lingji" parent="android:style/Theme.Material.NoActionBar">
|
||||
<item name="android:fontFamily">sans</item>
|
||||
<item name="android:statusBarColor">#1E1B18</item>
|
||||
<item name="android:navigationBarColor">#1E1B18</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">灵机</string>
|
||||
<string name="development_build">内部构建 · 开发验证页</string>
|
||||
<string name="home_tagline">本机起卦 · 离线可用</string>
|
||||
<string name="seal_character">易</string>
|
||||
<string name="core_check_title">领域核心已接入</string>
|
||||
<string name="core_check_body">用固定六爻夹具验证卦象、变卦与动爻计算链路。</string>
|
||||
<string name="verify_casting_core">验证起卦核心</string>
|
||||
<string name="local_data_notice">当前验证页不写入记录。正式功能将默认保存在本机,只有明确发起 AI 解读时才会联网。</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="core_check_passed">计算通过</string>
|
||||
<string name="fixture_result">复 %1$d → 坤 %2$d</string>
|
||||
<string name="moving_line_result">初爻动</string>
|
||||
<string name="fixture_explanation">固定输入为初爻老阳、其余五爻少阴。结果来自纯 Kotlin 起卦核心,不是界面写死的演示值。</string>
|
||||
<string name="primary_hexagram_description">本卦复,第二十四卦</string>
|
||||
<string name="transformed_hexagram_description">变卦坤,第二卦</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Lingji" parent="android:style/Theme.Material.Light.NoActionBar">
|
||||
<item name="android:fontFamily">sans</item>
|
||||
<item name="android:statusBarColor">#F5F0E6</item>
|
||||
<item name="android:navigationBarColor">#F5F0E6</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup disableIfNoEncryptionCapabilities="true">
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package brainwave.data.content
|
||||
package net.opcapp.flash.data.content
|
||||
|
||||
import brainwave.core.model.HexagramContent
|
||||
import net.opcapp.flash.core.model.HexagramContent
|
||||
|
||||
class FakeHexagramContentRepository(
|
||||
override val contentVersion: String = "fixture-only-v1",
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package brainwave.data.content
|
||||
package net.opcapp.flash.data.content
|
||||
|
||||
import brainwave.core.model.HexagramContent
|
||||
import net.opcapp.flash.core.model.HexagramContent
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package brainwave.domain.casting
|
||||
package net.opcapp.flash.domain.casting
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
Reference in New Issue
Block a user