Sprint 1 complete: hybrid Compose + LibGDX architecture building
Full AL-001 through AL-008 implementation: shared/ module with ModuleType enum, GameHost routing, StylusInputBridge, LineCorridorScreen with live stroke rendering, LineScore (straightness + anglePrecision), score returned to Compose hub via ActivityResult. Fixes LineScore field mismatch (pressureConsistency → anglePrecision) caught during AL-009 build check. Adds .gitignore and handoff notes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ plugins {
|
||||
}
|
||||
|
||||
val gdxVersion: String by project
|
||||
val composeBom: String by project
|
||||
val composeCompilerExtension: String by project
|
||||
val natives: Configuration by configurations.creating
|
||||
|
||||
android {
|
||||
@@ -27,6 +29,14 @@ android {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = composeCompilerExtension
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
assets.srcDirs(rootProject.file("assets"))
|
||||
@@ -37,24 +47,49 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation(project(":shared"))
|
||||
|
||||
// LibGDX
|
||||
implementation("com.badlogicgames.gdx:gdx-backend-android:$gdxVersion")
|
||||
natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a")
|
||||
natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a")
|
||||
natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86")
|
||||
natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64")
|
||||
|
||||
// Compose BOM
|
||||
val composeBomDep = platform("androidx.compose:compose-bom:$composeBom")
|
||||
implementation(composeBomDep)
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.material:material-icons-core")
|
||||
|
||||
// Activity + Navigation + Lifecycle
|
||||
implementation("androidx.activity:activity-compose:1.9.2")
|
||||
implementation("androidx.navigation:navigation-compose:2.8.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")
|
||||
|
||||
// Material XML themes (for Activity window styling)
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
}
|
||||
|
||||
tasks.register("copyAndroidNatives") {
|
||||
doFirst {
|
||||
val validAbis = setOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
natives.files.forEach { jar ->
|
||||
val outputDir = file("libs")
|
||||
outputDir.mkdirs()
|
||||
copy {
|
||||
from(zipTree(jar))
|
||||
into(outputDir)
|
||||
include("*.so")
|
||||
eachFile { path = name }
|
||||
includeEmptyDirs = false
|
||||
// Extract ABI from classifier, e.g. "gdx-platform-1.12.1-natives-arm64-v8a.jar" → "arm64-v8a"
|
||||
val abi = jar.name.substringAfter("natives-").removeSuffix(".jar")
|
||||
if (abi in validAbis) {
|
||||
val outputDir = file("libs/$abi")
|
||||
outputDir.mkdirs()
|
||||
copy {
|
||||
from(zipTree(jar))
|
||||
into(outputDir)
|
||||
include("*.so")
|
||||
eachFile { path = name }
|
||||
includeEmptyDirs = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
android/libs/arm64-v8a/libgdx.so
Executable file
BIN
android/libs/arm64-v8a/libgdx.so
Executable file
Binary file not shown.
BIN
android/libs/armeabi-v7a/libgdx.so
Executable file
BIN
android/libs/armeabi-v7a/libgdx.so
Executable file
Binary file not shown.
BIN
android/libs/x86/libgdx.so
Executable file
BIN
android/libs/x86/libgdx.so
Executable file
Binary file not shown.
BIN
android/libs/x86_64/libgdx.so
Executable file
BIN
android/libs/x86_64/libgdx.so
Executable file
Binary file not shown.
@@ -10,18 +10,28 @@
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/GdxTheme">
|
||||
android:theme="@style/Theme.ArtLegend">
|
||||
|
||||
<!-- Compose hub — LAUNCHER entry point -->
|
||||
<activity
|
||||
android:name=".AndroidLauncher"
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout"
|
||||
android:exported="true"
|
||||
android:screenOrientation="fullSensor">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- LibGDX game activity — launched by Intent from MainActivity -->
|
||||
<activity
|
||||
android:name=".GameActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout"
|
||||
android:exported="false"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@style/Theme.ArtLegend.Fullscreen" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.artlegend
|
||||
|
||||
import android.os.Bundle
|
||||
import com.badlogic.gdx.backends.android.AndroidApplication
|
||||
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration
|
||||
|
||||
class AndroidLauncher : AndroidApplication() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val config = AndroidApplicationConfiguration().apply {
|
||||
useImmersiveMode = true
|
||||
useGyroscope = false
|
||||
useAccelerometer = false
|
||||
}
|
||||
initialize(ArtLegend(), config)
|
||||
}
|
||||
}
|
||||
65
android/src/main/kotlin/com/artlegend/GameActivity.kt
Normal file
65
android/src/main/kotlin/com/artlegend/GameActivity.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package com.artlegend
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import com.artlegend.input.StylusFrame
|
||||
import com.artlegend.shared.ModuleType
|
||||
import com.badlogic.gdx.backends.android.AndroidApplication
|
||||
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration
|
||||
|
||||
class GameActivity : AndroidApplication() {
|
||||
private lateinit var gameHost: GameHost
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val moduleName = intent.getStringExtra("MODULE_TYPE") ?: ModuleType.LINE_CORRIDOR.name
|
||||
val moduleType = ModuleType.valueOf(moduleName)
|
||||
|
||||
gameHost = GameHost(moduleType)
|
||||
val config = AndroidApplicationConfiguration().apply {
|
||||
useImmersiveMode = true
|
||||
useGyroscope = false
|
||||
useAccelerometer = false
|
||||
}
|
||||
initialize(gameHost, config)
|
||||
|
||||
// Wire result callback — invoked on GL thread, must switch to UI thread.
|
||||
gameHost.onSessionComplete = { score ->
|
||||
runOnUiThread {
|
||||
val result = Intent().apply {
|
||||
putExtra("straightness", score.straightness)
|
||||
putExtra("anglePrecision", score.anglePrecision)
|
||||
putExtra("overall", score.overall)
|
||||
}
|
||||
setResult(Activity.RESULT_OK, result)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Capture stylus data on LibGDX's GL view; return false so LibGDX also sees the event.
|
||||
graphics.view.setOnTouchListener { _, event ->
|
||||
handleStylusEvent(event)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStylusEvent(event: MotionEvent) {
|
||||
val isDown = event.action != MotionEvent.ACTION_UP &&
|
||||
event.action != MotionEvent.ACTION_CANCEL
|
||||
val frame = StylusFrame(
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
pressure = event.pressure,
|
||||
tiltRad = event.getAxisValue(MotionEvent.AXIS_TILT),
|
||||
orientRad = event.getAxisValue(MotionEvent.AXIS_ORIENTATION),
|
||||
isStylus = event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS,
|
||||
isDown = isDown
|
||||
)
|
||||
android.util.Log.d("GameActivity", "pressure=%.3f isStylus=${frame.isStylus} isDown=$isDown"
|
||||
.format(frame.pressure))
|
||||
gameHost.bridge.post(frame)
|
||||
}
|
||||
}
|
||||
48
android/src/main/kotlin/com/artlegend/MainActivity.kt
Normal file
48
android/src/main/kotlin/com/artlegend/MainActivity.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.artlegend
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.artlegend.modules.LineScore
|
||||
import com.artlegend.shared.ModuleType
|
||||
import com.artlegend.ui.ArtLegendApp
|
||||
import com.artlegend.ui.theme.ArtLegendTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private var lastScore: LineScore? by mutableStateOf(null)
|
||||
|
||||
private val gameResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
val data = result.data ?: return@registerForActivityResult
|
||||
lastScore = LineScore(
|
||||
straightness = data.getFloatExtra("straightness", 0f),
|
||||
anglePrecision = data.getFloatExtra("anglePrecision", 0f),
|
||||
overall = data.getFloatExtra("overall", 0f)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
ArtLegendTheme {
|
||||
ArtLegendApp(
|
||||
onLaunchModule = { moduleType ->
|
||||
lastScore = null // clear previous score when launching
|
||||
val intent = Intent(this, GameActivity::class.java).apply {
|
||||
putExtra("MODULE_TYPE", moduleType.name)
|
||||
}
|
||||
gameResultLauncher.launch(intent)
|
||||
},
|
||||
lastScore = lastScore
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
android/src/main/kotlin/com/artlegend/ui/ArtLegendApp.kt
Normal file
46
android/src/main/kotlin/com/artlegend/ui/ArtLegendApp.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.artlegend.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.artlegend.modules.LineScore
|
||||
import com.artlegend.shared.ModuleType
|
||||
import com.artlegend.ui.screens.HubScreen
|
||||
import com.artlegend.ui.screens.ModuleDetailScreen
|
||||
import com.artlegend.ui.screens.ProgressScreen
|
||||
import com.artlegend.ui.screens.SettingsScreen
|
||||
|
||||
@Composable
|
||||
fun ArtLegendApp(
|
||||
onLaunchModule: (ModuleType) -> Unit,
|
||||
lastScore: LineScore?
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController = navController, startDestination = "hub") {
|
||||
composable("hub") {
|
||||
HubScreen(
|
||||
onModuleClick = { moduleType -> navController.navigate("detail/${moduleType.name}") },
|
||||
onProgressClick = { navController.navigate("progress") },
|
||||
onSettingsClick = { navController.navigate("settings") }
|
||||
)
|
||||
}
|
||||
composable("detail/{moduleType}") { backStackEntry ->
|
||||
val moduleType = ModuleType.valueOf(
|
||||
backStackEntry.arguments?.getString("moduleType") ?: ModuleType.LINE_CORRIDOR.name
|
||||
)
|
||||
ModuleDetailScreen(
|
||||
moduleType = moduleType,
|
||||
lastScore = lastScore,
|
||||
onStartTraining = { onLaunchModule(moduleType) },
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable("progress") {
|
||||
ProgressScreen(onBack = { navController.popBackStack() })
|
||||
}
|
||||
composable("settings") {
|
||||
SettingsScreen(onBack = { navController.popBackStack() })
|
||||
}
|
||||
}
|
||||
}
|
||||
102
android/src/main/kotlin/com/artlegend/ui/screens/HubScreen.kt
Normal file
102
android/src/main/kotlin/com/artlegend/ui/screens/HubScreen.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package com.artlegend.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.artlegend.shared.ModuleType
|
||||
|
||||
private data class ModuleCard(
|
||||
val type: ModuleType,
|
||||
val title: String,
|
||||
val description: String
|
||||
)
|
||||
|
||||
private val modules = listOf(
|
||||
ModuleCard(ModuleType.LINE_CORRIDOR, "Line Corridor", "Master straight lines under pressure"),
|
||||
ModuleCard(ModuleType.PRESSURE_RAMP, "Pressure Ramp", "Control stroke weight dynamically"),
|
||||
ModuleCard(ModuleType.CURVE_SCULPTOR, "Curve Sculptor", "Shape smooth bezier curves"),
|
||||
ModuleCard(ModuleType.VECTOR_SNAP, "Vector Snap", "Hit precise vector endpoints")
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HubScreen(
|
||||
onModuleClick: (ModuleType) -> Unit,
|
||||
onProgressClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("ArtLegend") },
|
||||
actions = {
|
||||
IconButton(onClick = onSettingsClick) {
|
||||
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
NavigationBarItem(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
icon = { Text("Train") },
|
||||
label = { Text("Train") }
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = false,
|
||||
onClick = onProgressClick,
|
||||
icon = { Text("Progress") },
|
||||
label = { Text("Progress") }
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
items(modules) { module ->
|
||||
Card(
|
||||
onClick = { onModuleClick(module.type) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(text = module.title, style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = module.description, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.artlegend.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.artlegend.modules.LineScore
|
||||
import com.artlegend.shared.ModuleType
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ModuleDetailScreen(
|
||||
moduleType: ModuleType,
|
||||
lastScore: LineScore?,
|
||||
onStartTraining: () -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val title = moduleType.name.split('_').joinToString(" ") { word ->
|
||||
word.lowercase().replaceFirstChar { it.uppercase() }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(title) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(text = title, style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = when (moduleType) {
|
||||
ModuleType.LINE_CORRIDOR ->
|
||||
"Train your hand to draw perfectly straight lines using S-Pen pressure " +
|
||||
"feedback. Keep your stroke within the corridor to score points."
|
||||
ModuleType.PRESSURE_RAMP ->
|
||||
"Learn to control stroke weight through gradual pressure ramping exercises. " +
|
||||
"Match the target pressure curve for a perfect score."
|
||||
ModuleType.CURVE_SCULPTOR ->
|
||||
"Master the art of smooth curve drawing with guided bezier targets. " +
|
||||
"Follow the path precisely to develop muscle memory."
|
||||
ModuleType.VECTOR_SNAP ->
|
||||
"Develop precision for hitting exact vector endpoints and anchor points. " +
|
||||
"Speed and accuracy both contribute to your score."
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
|
||||
if (lastScore != null) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
ScoreCard(lastScore)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onStartTraining,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
) {
|
||||
Text(if (lastScore != null) "Train Again" else "Start Training")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScoreCard(score: LineScore) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("Last Session", style = MaterialTheme.typography.labelMedium)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
ScoreStat("Overall", score.overall)
|
||||
ScoreStat("Line", score.straightness)
|
||||
ScoreStat("Angle", score.anglePrecision)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScoreStat(label: String, value: Float) {
|
||||
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "${(value * 100).toInt()}%",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.artlegend.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ProgressScreen(onBack: () -> Unit) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Progress") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Progress tracking coming soon", style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.artlegend.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(onBack: () -> Unit) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Settings coming soon", style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.artlegend.ui.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val Violet = Color(0xFF7B2FBE)
|
||||
private val VioletContainer = Color(0xFF4A0E8F)
|
||||
private val Cyan = Color(0xFF00E5FF)
|
||||
private val CyanContainer = Color(0xFF004D57)
|
||||
private val DarkBackground = Color(0xFF0D0D12)
|
||||
private val DarkSurface = Color(0xFF16161F)
|
||||
|
||||
private val DarkColors = darkColorScheme(
|
||||
primary = Violet,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = VioletContainer,
|
||||
onPrimaryContainer = Cyan,
|
||||
secondary = Cyan,
|
||||
onSecondary = Color.Black,
|
||||
secondaryContainer = CyanContainer,
|
||||
background = DarkBackground,
|
||||
surface = DarkSurface,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ArtLegendTheme(content: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
colorScheme = DarkColors,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="GdxTheme" parent="android:Theme.NoTitleBar.Fullscreen" />
|
||||
</resources>
|
||||
11
android/src/main/res/values/themes.xml
Normal file
11
android/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Main Compose activity: Material3 dark, no action bar -->
|
||||
<style name="Theme.ArtLegend" parent="Theme.Material3.Dark.NoActionBar" />
|
||||
|
||||
<!-- LibGDX game activity: fullscreen, no action bar -->
|
||||
<style name="Theme.ArtLegend.Fullscreen" parent="Theme.Material3.Dark.NoActionBar">
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user