diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..d1940af --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "net.leodesouza.blitz" + compileSdk = 34 + + defaultConfig { + applicationId = "net.leodesouza.blitz" + minSdk = 23 + targetSdk = 34 + versionCode = 100 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = true + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:2023.08.00")) + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + debugImplementation("androidx.compose.ui:ui-tooling") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ff7170b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/app/src/main/java/net/leodesouza/blitz/MainActivity.kt b/app/src/main/java/net/leodesouza/blitz/MainActivity.kt new file mode 100644 index 0000000..0a1db3d --- /dev/null +++ b/app/src/main/java/net/leodesouza/blitz/MainActivity.kt @@ -0,0 +1,148 @@ +package net.leodesouza.blitz + +import android.content.pm.ActivityInfo +import android.os.Build +import android.os.Bundle +import android.os.SystemClock.elapsedRealtime +import android.text.format.DateUtils.formatElapsedTime +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + window.attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + super.onCreate(savedInstanceState) + setContent { + CountDown(303_000L, 3_000L) { whiteTime, blackTime, onClick -> + ChessClock(whiteTime, blackTime, onClick) + } + } + } +} + +@Composable +fun CountDown( + duration: Long, + increment: Long, + content: @Composable (whiteTime: Long, blackTime: Long, onClick: () -> Unit) -> Unit +) { + var whiteTime by remember { mutableLongStateOf(duration) } + var blackTime by remember { mutableLongStateOf(duration) } + var endTime by remember { mutableLongStateOf(elapsedRealtime() + duration) } + var timesPressed by remember { mutableIntStateOf(0) } + var isPressed by remember { mutableStateOf(false) } + var isRunning by remember { mutableStateOf(false) } + val onClick = { + timesPressed++ + isPressed = true + } + content.invoke(whiteTime, blackTime, onClick) + BackHandler(isRunning) { + timesPressed-- + isRunning = false + } + LaunchedEffect(whiteTime, blackTime, isPressed) { + val isBlackTurn = (timesPressed % 2 == 0) + if (isPressed) { + if (isRunning) { + if (isBlackTurn) { + whiteTime += increment + } else { + blackTime += increment + } + } else { + isRunning = true + } + endTime = elapsedRealtime() + if (isBlackTurn) blackTime else whiteTime + isPressed = false + } + if (isRunning) { + val remainingTime = if (elapsedRealtime() < endTime) { + delay((endTime - elapsedRealtime()) % 100L) + endTime - elapsedRealtime() + } else { + 0L + } + if (isBlackTurn) blackTime = remainingTime + else whiteTime = remainingTime + } + } +} + +@Composable +fun ChessClock(whiteTime: Long, blackTime: Long, onClick: () -> Unit) { + Surface(onClick) { + Row { + Time( + whiteTime, + color = if (whiteTime == 0L) Color.Red else Color.Black, + modifier = Modifier + .background(Color.White) + .weight(1F) + .fillMaxSize() + .wrapContentSize() + ) + Time( + blackTime, + color = if (blackTime == 0L) Color.Red else Color.White, + modifier = Modifier + .background(Color.Black) + .weight(1F) + .fillMaxSize() + .wrapContentSize() + ) + } + } +} + +@Composable +fun Time(timeMillis: Long, color: Color, modifier: Modifier = Modifier) { + val roundedTime = (timeMillis + 50L) / 100L + val integerPart = formatElapsedTime(roundedTime / 10L) + val decimalPart = roundedTime % 10L + Text( + text = "$integerPart.$decimalPart", + modifier = modifier, + color = color, + fontSize = with(LocalDensity.current) { + (LocalConfiguration.current.screenWidthDp / 8).dp.toSp() + }, + ) +} + +@Preview +@Composable +fun ChessClockPreview() { + CountDown(13_000L, 3_000L) { whiteTime, blackTime, onClick -> + ChessClock(whiteTime, blackTime, onClick) + } +} diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..6eda075 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..5ed0a2d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..5ed0a2d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..989b88e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..59e0aeb Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..59810b6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..ad7c622 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..da2ce2f Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..0bb6f8a Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..badd48e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b435c64 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..802700d Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..3e57886 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..f42ada6 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..ef57823 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +