diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dcde67a..146f8c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") } android { @@ -64,8 +65,9 @@ dependencies { implementation("androidx.compose.material3:material3") implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-moshi:2.9.0") - - + implementation("androidx.room:room-runtime:2.6.1") + annotationProcessor("androidx.room:room-compiler:2.6.1") + ksp("androidx.room:room-compiler:2.6.1") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/app/src/main/java/com/manhtai/whatthefoto/Configuration.kt b/app/src/main/java/com/manhtai/whatthefoto/Configuration.kt index 6f438f8..bf7171a 100644 --- a/app/src/main/java/com/manhtai/whatthefoto/Configuration.kt +++ b/app/src/main/java/com/manhtai/whatthefoto/Configuration.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.provider.Settings +import android.util.Patterns import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -25,22 +26,66 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import android.graphics.Color as Colour import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.core.content.ContextCompat +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase import java.util.Calendar +@Entity data class Config( - var defaultImage: String = "https://cdn2.thecatapi.com/images/7_rjG2-pc.jpg", - var apiURL: String = "https://api.thecatapi.com/v1/images/search?limit=10", - var imageDelaySeconds: Int = 30, - var imageFadeSeconds: Int = 3, - var sleepFromHour: Int = 19, - var sleepToHour: Int = 8, + val defaultImage: String = "https://cdn2.thecatapi.com/images/7_rjG2-pc.jpg", + + @PrimaryKey val id: Int = 1, + @ColumnInfo(name = "api_url") val apiURL: String = "https://api.thecatapi.com/v1/images/search?limit=10", + @ColumnInfo(name = "image_delay_seconds") val imageDelaySeconds: Int = 30, + @ColumnInfo(name = "image_fade_seconds") val imageFadeSeconds: Int = 3, + @ColumnInfo(name = "sleep_from_hour") val sleepFromHour: Int = 19, + @ColumnInfo(name = "sleep_to_hour") val sleepToHour: Int = 8, + @ColumnInfo(name = "background_color") val backgroundColor: String = "#000000", ) +@Dao +interface ConfigDao { + @Query("SELECT * FROM config WHERE id = 1") + fun get(): Config? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(conf: Config) +} + +@Database(entities = [Config::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + abstract fun configDao(): ConfigDao + + companion object { + @Volatile + private var Instance: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + // if the Instance is not null, return it, otherwise create a new database instance. + return Instance ?: synchronized(this) { + Room.databaseBuilder(context, AppDatabase::class.java, "local-database") + .build() + .also { Instance = it } + } + } + } +} + + @Composable fun ConfigurationPopup( ctx: Context, @@ -49,10 +94,17 @@ fun ConfigurationPopup( oldConf: Config, ) { var conf by remember { mutableStateOf(oldConf) } + var apiURL by remember { mutableStateOf(oldConf.apiURL) } + var delaySeconds by remember { mutableStateOf(oldConf.imageDelaySeconds.toString()) } + var fadeSeconds by remember { mutableStateOf(oldConf.imageFadeSeconds.toString()) } + var sleepFrom by remember { mutableStateOf(oldConf.sleepFromHour.toString()) } + var sleepTo by remember { mutableStateOf(oldConf.sleepToHour.toString()) } + var bgColor by remember { mutableStateOf(oldConf.backgroundColor) } + var msg by remember { mutableStateOf("") } Dialog( onDismissRequest = onDismiss, - properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true) + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) ) { Box( modifier = Modifier @@ -79,8 +131,8 @@ fun ConfigurationPopup( } TextField( - value = conf.apiURL, - onValueChange = { conf = conf.copy(apiURL = it) }, + value = apiURL, + onValueChange = { apiURL = it }, label = { Text("Image API URL (return [{ url }])") }, modifier = Modifier .fillMaxWidth() @@ -93,21 +145,20 @@ fun ConfigurationPopup( .fillMaxWidth() .padding(8.dp) ) { - TextField(value = conf.imageDelaySeconds.toString(), + TextField(value = delaySeconds, label = { Text(text = "Image delay (seconds)") }, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), onValueChange = { - conf = - conf.copy(imageDelaySeconds = if (it != "" && it.toInt() > 0) it.toInt() else 1) + delaySeconds = it }) - TextField(value = conf.imageFadeSeconds.toString(), + TextField(value = fadeSeconds, label = { Text(text = "Image fade (seconds)") }, modifier = Modifier .padding(start = 8.dp), keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), onValueChange = { - conf = conf.copy(imageFadeSeconds = if (it != "") it.toInt() else 0) + fadeSeconds = it }) } @@ -116,17 +167,14 @@ fun ConfigurationPopup( .fillMaxWidth() .padding(8.dp) ) { - TextField(value = conf.sleepFromHour.toString(), + TextField(value = sleepFrom, label = { Text(text = "Sleep from hour (0h-24h)") }, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), onValueChange = { - conf = conf.copy( - sleepFromHour = - if (it != "" && it.toInt() in 0..24) it.toInt() else 0 - ) + sleepFrom = it }) - TextField(value = conf.sleepToHour.toString(), + TextField(value = sleepTo, label = { Text( text = "To hour (0h-24h). Now is " + Calendar.getInstance() @@ -136,10 +184,20 @@ fun ConfigurationPopup( modifier = Modifier.padding(start = 8.dp), keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), onValueChange = { - conf = conf.copy( - sleepToHour = - if (it != "" && it.toInt() in 0..24) it.toInt() else 0 - ) + sleepTo = it + }) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + TextField(value = bgColor, + label = { Text(text = "Background color") }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text), + onValueChange = { + bgColor = it }) } @@ -156,13 +214,56 @@ fun ConfigurationPopup( Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { - onSave(conf) - onDismiss() + if ( + apiURL != "" && isValidURL(apiURL) && + delaySeconds != "" && + fadeSeconds != "" && + sleepFrom != "" && sleepFrom.toInt() < 24 && + sleepTo != "" && sleepTo.toInt() < 24 && + bgColor != "" && isValidColor(bgColor) + ) { + conf = conf.copy( + apiURL = apiURL, + imageDelaySeconds = delaySeconds.toInt(), + imageFadeSeconds = fadeSeconds.toInt(), + sleepFromHour = sleepFrom.toInt(), + sleepToHour = sleepTo.toInt(), + backgroundColor = bgColor + ) + onSave(conf) + onDismiss() + } else { + msg = "Invalid configuration!" + } }) { Text("Save") } } + + if (msg != "") { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.End + ) { + Text(text = msg, color = Color.Red) + } + } } } } } + +fun isValidColor(c: String): Boolean { + return try { + Colour.parseColor(c) + true + } catch (_: Exception) { + false + } +} + +fun isValidURL(u: String): Boolean { + return Patterns.WEB_URL.matcher(u).matches() +} \ No newline at end of file diff --git a/app/src/main/java/com/manhtai/whatthefoto/MainActivity.kt b/app/src/main/java/com/manhtai/whatthefoto/MainActivity.kt index cdc3e23..17d14d9 100644 --- a/app/src/main/java/com/manhtai/whatthefoto/MainActivity.kt +++ b/app/src/main/java/com/manhtai/whatthefoto/MainActivity.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -22,6 +23,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import android.graphics.Color as Colour import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -52,25 +54,34 @@ class MainActivity : ComponentActivity() { @Composable fun Main(photoAPI: PhotoApiService = PhotoApi.service) { + val TAG = "Main" + val context = LocalContext.current + val coroutineScope: CoroutineScope = rememberCoroutineScope() + var conf by remember { mutableStateOf(Config()) } var imageUrl by remember { mutableStateOf(conf.defaultImage) } var isConfigPopupVisible by remember { mutableStateOf(false) } var isScreenOn by remember { mutableStateOf(true) } - - val TAG = "Main" - val context = LocalContext.current - val coroutineScope: CoroutineScope = rememberCoroutineScope() + var bgColor by remember { mutableIntStateOf(Colour.parseColor(conf.backgroundColor)) } // Start periodic updates LaunchedEffect(Unit) { coroutineScope.launch(Dispatchers.IO) { + val db = AppDatabase.getDatabase(context = context) + val dbConf = db.configDao().get() + if (dbConf == null) { + db.configDao().insert(conf) + } else { + conf = dbConf + } + while (true) { // Sleep val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) isScreenOn = if (conf.sleepFromHour < conf.sleepToHour) { hour < conf.sleepFromHour || hour > conf.sleepToHour } else { - hour in conf.sleepToHour..conf.sleepFromHour + hour in conf.sleepToHour + 1.. conf = c + bgColor = Colour.parseColor(c.backgroundColor) + coroutineScope.launch(Dispatchers.IO) { + val db = AppDatabase.getDatabase(context = context) + db.configDao().insert(conf) + } }, conf ) diff --git a/build.gradle.kts b/build.gradle.kts index a6711ae..e66c609 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { id("com.android.application") version "8.2.1" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false + id("com.google.devtools.ksp") version "1.9.22-1.0.16" apply false } \ No newline at end of file