master
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
58
app/build.gradle.kts
Normal file
@@ -0,0 +1,58 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.playninescore"
|
||||
compileSdk {
|
||||
version = release(36) {
|
||||
minorApiLevel = 1
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.playninescore"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.playninescore
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.playninescore", appContext.packageName)
|
||||
}
|
||||
}
|
||||
27
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.PlayNineScore">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.PlayNineScore">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
438
app/src/main/java/com/example/playninescore/MainActivity.kt
Normal file
@@ -0,0 +1,438 @@
|
||||
package com.example.playninescore
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.playninescore.ui.theme.PlayNineScoreTheme
|
||||
|
||||
// Thème de couleurs Mini Golf
|
||||
val GolfGreen = Color(0xFF2E7D32)
|
||||
val FairwayGreen = Color(0xFF4CAF50)
|
||||
val FairwayGreenDark = Color(0xFF388E3C)
|
||||
val GrassLight = Color(0xFFE8F5E9)
|
||||
val FlagRed = Color(0xFFD32F2F)
|
||||
val GolfBallWhite = Color(0xFFF5F5F5)
|
||||
|
||||
data class Player(
|
||||
val name: String = "",
|
||||
val scores: List<String?> = List(9) { null }
|
||||
)
|
||||
|
||||
val PlayerListSaver: Saver<List<Player>, Any> = listSaver(
|
||||
save = { list ->
|
||||
list.map { player ->
|
||||
listOf(player.name) + player.scores.map { it ?: "" }
|
||||
}
|
||||
},
|
||||
restore = { restored ->
|
||||
val list = restored as? List<*>
|
||||
list?.map { item ->
|
||||
val data = item as List<*>
|
||||
Player(
|
||||
name = data[0] as String,
|
||||
scores = data.drop(1).map { val s = it as String; if (s.isEmpty()) null else s }
|
||||
)
|
||||
} ?: emptyList()
|
||||
}
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun FairwayBackground(content: @Composable () -> Unit) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Dessin des bandes du fairway
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val stripeWidth = 80.dp.toPx()
|
||||
var x = 0f
|
||||
var index = 0
|
||||
while (x < size.width) {
|
||||
drawRect(
|
||||
color = if (index % 2 == 0) FairwayGreen else FairwayGreenDark,
|
||||
topLeft = Offset(x, 0f),
|
||||
size = Size(stripeWidth, size.height)
|
||||
)
|
||||
x += stripeWidth
|
||||
index++
|
||||
}
|
||||
|
||||
// Texture d'herbe légère par-dessus
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(Color.Black.copy(alpha = 0.05f), Color.Transparent, Color.Black.copy(alpha = 0.05f))
|
||||
)
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GolfBallImage(modifier: Modifier = Modifier) {
|
||||
Canvas(modifier = modifier.size(60.dp)) {
|
||||
// Ombre portée
|
||||
drawOval(
|
||||
color = Color.Black.copy(alpha = 0.2f),
|
||||
topLeft = Offset(size.width * 0.1f, size.height * 0.75f),
|
||||
size = Size(size.width * 0.8f, size.height * 0.25f)
|
||||
)
|
||||
// Corps de la balle
|
||||
drawCircle(color = Color.White)
|
||||
drawCircle(color = Color.LightGray, radius = size.minDimension / 2f, style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.dp.toPx()))
|
||||
|
||||
// Petites alvéoles (dimples)
|
||||
val rows = 5
|
||||
val cols = 5
|
||||
for (i in 1 until rows) {
|
||||
for (j in 1 until cols) {
|
||||
drawCircle(
|
||||
color = Color.LightGray.copy(alpha = 0.3f),
|
||||
radius = 2.dp.toPx(),
|
||||
center = Offset(size.width * (i/rows.toFloat()), size.height * (j/cols.toFloat()))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GolfFlagImage(modifier: Modifier = Modifier) {
|
||||
Canvas(modifier = modifier.size(40.dp)) {
|
||||
val poleWidth = 2.dp.toPx()
|
||||
// Le trou
|
||||
drawOval(Color.Black.copy(alpha = 0.3f), Offset(0f, size.height * 0.8f), Size(size.width, size.height * 0.2f))
|
||||
// Le mât
|
||||
drawRect(Color.DarkGray, Offset(size.width/2 - poleWidth/2, 10f), Size(poleWidth, size.height * 0.85f))
|
||||
// Le drapeau
|
||||
val path = Path().apply {
|
||||
moveTo(size.width/2, 10f)
|
||||
lineTo(size.width * 0.9f, size.height * 0.25f)
|
||||
lineTo(size.width/2, size.height * 0.4f)
|
||||
close()
|
||||
}
|
||||
drawPath(path, FlagRed)
|
||||
}
|
||||
}
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
PlayNineScoreTheme {
|
||||
FairwayBackground {
|
||||
PlayNineApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlayNineApp() {
|
||||
var players by rememberSaveable(stateSaver = PlayerListSaver) {
|
||||
mutableStateOf(listOf<Player>())
|
||||
}
|
||||
var gameStarted by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
containerColor = Color.Transparent
|
||||
) { innerPadding ->
|
||||
val modifier = Modifier.padding(innerPadding)
|
||||
|
||||
if (!gameStarted) {
|
||||
SetupScreen(
|
||||
players = players,
|
||||
onPlayersChange = { players = it },
|
||||
onStartGame = { if (players.isNotEmpty()) gameStarted = true },
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
ScoreScreen(
|
||||
players = players,
|
||||
onScoresChange = { players = it },
|
||||
onReset = {
|
||||
players = emptyList()
|
||||
gameStarted = false
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetupScreen(
|
||||
players: List<Player>,
|
||||
onPlayersChange: (List<Player>) -> Unit,
|
||||
onStartGame: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var newPlayerName by remember { mutableStateOf("") }
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize().padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
GolfBallImage()
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
"PLAY NINE",
|
||||
fontSize = 38.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
color = Color.White,
|
||||
style = TextStyle(
|
||||
shadow = androidx.compose.ui.graphics.Shadow(Color.Black, Offset(2f, 2f), 4f)
|
||||
)
|
||||
)
|
||||
Text("Le compteur de score ultime", fontSize = 14.sp, color = Color.White.copy(alpha = 0.9f))
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.95f)),
|
||||
elevation = CardDefaults.cardElevation(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextField(
|
||||
value = newPlayerName,
|
||||
onValueChange = { newPlayerName = it },
|
||||
placeholder = { Text("Nom du joueur...") },
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = GolfGreen
|
||||
),
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (newPlayerName.isNotBlank() && players.size < 6) {
|
||||
onPlayersChange(players + Player(name = newPlayerName))
|
||||
newPlayerName = ""
|
||||
}
|
||||
},
|
||||
colors = IconButtonDefaults.iconButtonColors(contentColor = GolfGreen)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Ajouter")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Box(modifier = Modifier.weight(1f, fill = false).padding(vertical = 8.dp)) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
itemsIndexed(players) { index, player ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.85f))
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(player.name, fontWeight = FontWeight.Bold, color = GolfGreen) },
|
||||
leadingContent = {
|
||||
Box(Modifier.size(32.dp).background(GolfGreen, CircleShape), contentAlignment = Alignment.Center) {
|
||||
Text("${index + 1}", color = Color.White, fontSize = 12.sp)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = {
|
||||
val list = players.toMutableList()
|
||||
list.removeAt(index)
|
||||
onPlayersChange(list)
|
||||
}) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Supprimer", tint = FlagRed)
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = onStartGame,
|
||||
modifier = Modifier.fillMaxWidth().height(60.dp),
|
||||
enabled = players.isNotEmpty(),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = GolfGreen),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = ButtonDefaults.buttonElevation(4.dp)
|
||||
) {
|
||||
Text("ENTRER SUR LE GREEN", fontSize = 18.sp, fontWeight = FontWeight.Black)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScoreScreen(
|
||||
players: List<Player>,
|
||||
onScoresChange: (List<Player>) -> Unit,
|
||||
onReset: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val holeW = 60.dp
|
||||
val playerW = 100.dp
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize().padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// En-tête
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
GolfFlagImage(Modifier.size(35.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"Scorecard",
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
color = Color.White,
|
||||
style = TextStyle(
|
||||
shadow = androidx.compose.ui.graphics.Shadow(Color.Black, Offset(2f, 2f), 4f)
|
||||
)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onReset,
|
||||
colors = IconButtonDefaults.iconButtonColors(containerColor = Color.White.copy(alpha = 0.2f))
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Reset", tint = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
// Centre de l'écran : La Card de score
|
||||
Box(
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.wrapContentHeight().fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.95f)),
|
||||
elevation = CardDefaults.cardElevation(8.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.horizontalScroll(scrollState)) {
|
||||
Column {
|
||||
// Header du tableau
|
||||
Row(modifier = Modifier.background(GolfGreen).padding(vertical = 10.dp)) {
|
||||
Text("Trou", Modifier.width(holeW), color = Color.White, textAlign = TextAlign.Center, fontWeight = FontWeight.Black)
|
||||
players.forEach { player ->
|
||||
Text(player.name, Modifier.width(playerW), color = Color.White, textAlign = TextAlign.Center, fontWeight = FontWeight.Black, maxLines = 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Contenu (Scrollable verticalement seulement si nécessaire)
|
||||
val verticalScrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.verticalScroll(verticalScrollState)) {
|
||||
for (hIdx in 0 until 9) {
|
||||
val isEven = hIdx % 2 == 0
|
||||
Row(
|
||||
modifier = Modifier.background(if (isEven) Color.Transparent else GolfGreen.copy(alpha = 0.05f)),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("${hIdx + 1}", Modifier.width(holeW).padding(vertical = 16.dp), textAlign = TextAlign.Center, fontWeight = FontWeight.ExtraBold, color = GolfGreen)
|
||||
players.forEachIndexed { pIdx, player ->
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier.width(playerW).height(55.dp)
|
||||
.background(if (isFocused) FairwayGreen.copy(alpha = 0.2f) else Color.Transparent)
|
||||
.border(0.5.dp, Color.LightGray.copy(alpha = 0.3f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
BasicTextField(
|
||||
value = player.scores[hIdx] ?: "",
|
||||
onValueChange = { nv ->
|
||||
if (nv.isEmpty() || nv == "-" || nv.toIntOrNull() != null) {
|
||||
val upP = players.toMutableList()
|
||||
val upS = player.scores.toMutableList()
|
||||
upS[hIdx] = nv
|
||||
upP[pIdx] = player.copy(scores = upS)
|
||||
onScoresChange(upP)
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
textStyle = TextStyle(textAlign = TextAlign.Center, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = if(isFocused) GolfGreen else Color.DarkGray),
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { isFocused = it.isFocused },
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Total Row
|
||||
Row(modifier = Modifier.background(FairwayGreen).padding(vertical = 14.dp)) {
|
||||
Text("TOTAL", Modifier.width(holeW), color = Color.White, textAlign = TextAlign.Center, fontWeight = FontWeight.Black)
|
||||
players.forEach { player ->
|
||||
val total = player.scores.sumOf { it?.toIntOrNull() ?: 0 }
|
||||
Text("$total", Modifier.width(playerW), color = Color.White, textAlign = TextAlign.Center, fontWeight = FontWeight.Black, fontSize = 22.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.playninescore.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.playninescore.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun PlayNineScoreTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
34
app/src/main/java/com/example/playninescore/ui/theme/Type.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.example.playninescore.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
15
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- Fond vert type fairway -->
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<!-- Bandes de tonte -->
|
||||
<path
|
||||
android:fillColor="#388E3C"
|
||||
android:pathData="M0,0h27v108h-27z M54,0h27v108h-27z" />
|
||||
</vector>
|
||||
43
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<!-- Ombre légère de la balle -->
|
||||
<path
|
||||
android:fillColor="#22000000"
|
||||
android:pathData="M54,58m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0" />
|
||||
|
||||
<!-- Corps de la balle de golf (blanc) -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,54m-34,0a34,34 0,1 1,68 0a34,34 0,1 1,-68 0" />
|
||||
|
||||
<!-- Bordure légère -->
|
||||
<path
|
||||
android:strokeColor="#E0E0E0"
|
||||
android:strokeWidth="0.5"
|
||||
android:pathData="M54,54m-34,0a34,34 0,1 1,68 0a34,34 0,1 1,-68 0" />
|
||||
|
||||
<!-- Alvéoles (dimples) -->
|
||||
<group android:translateX="54" android:translateY="54">
|
||||
<!-- Centre -->
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M0,0m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
|
||||
<!-- Cercle intérieur -->
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M-12,-8m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M12,-8m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M0,14m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
|
||||
<!-- Cercle extérieur -->
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M-22,0m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M22,0m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M-14,20m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M14,20m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M0,-22m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M-18,-18m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
<path android:fillColor="#EEEEEE" android:pathData="M18,-18m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
</group>
|
||||
|
||||
</vector>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
10
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Play nine score</string>
|
||||
</resources>
|
||||
5
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.PlayNineScore" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.playninescore
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||