From eeaf40d98a36fda6334610bb46d762061d659948 Mon Sep 17 00:00:00 2001 From: Tim Kluge Date: Sun, 1 Dec 2024 10:42:39 +0100 Subject: [PATCH] Add beep tone select, auto dismiss option --- .../karooreminder/KarooReminderExtension.kt | 16 ++- .../karooreminder/screens/DetailScreen.kt | 101 +++++++++++++++++- .../karooreminder/screens/MainScreen.kt | 11 +- .../timklge/karooreminder/screens/Reminder.kt | 20 +++- 4 files changed, 140 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt b/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt index 129789e..50fb829 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt @@ -1,6 +1,8 @@ package de.timklge.karooreminder +import android.util.Log import de.timklge.karooreminder.screens.Reminder +import de.timklge.karooreminder.screens.ReminderBeepPattern import de.timklge.karooreminder.screens.defaultReminders import de.timklge.karooreminder.screens.preferencesKey import io.hammerhead.karooext.KarooSystemService @@ -41,7 +43,14 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", "1.0") { } val preferences = applicationContext.dataStore.data.map { remindersJson -> - Json.decodeFromString>(remindersJson[preferencesKey] ?: defaultReminders) + try { + Json.decodeFromString>( + remindersJson[preferencesKey] ?: defaultReminders + ) + } catch(e: Throwable){ + Log.e("karoo-reminder","Failed to read preferences", e) + mutableListOf() + } } karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME) @@ -50,19 +59,20 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", "1.0") { .distinctUntilChanged() .drop(1) .combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders} + .distinctUntilChanged { old, new -> old.first == new.first } .collect { (elapsedMinutes, reminders) -> reminders .filter { reminder -> reminder.isActive && elapsedMinutes % reminder.interval == 0 } .forEach { reminder -> karooSystem.dispatch(TurnScreenOn) delay(1_000) - karooSystem.dispatch(PlayBeepPattern(listOf(PlayBeepPattern.Tone(300, 200), PlayBeepPattern.Tone(500, 200), PlayBeepPattern.Tone(700, 200)))) + if (reminder.tone != ReminderBeepPattern.NO_TONES) karooSystem.dispatch(PlayBeepPattern(reminder.tone.tones)) karooSystem.dispatch( InRideAlert( id = "reminder-${reminder.id}-${elapsedMinutes}", detail = reminder.text, title = reminder.name, - autoDismissMs = 15_000, + autoDismissMs = if(reminder.isAutoDismiss) 15_000 else null, icon = R.drawable.ic_launcher, textColor = R.color.white, backgroundColor = reminder.getResourceColor(applicationContext) diff --git a/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt b/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt index 970a90e..a3b6f01 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt @@ -1,6 +1,7 @@ package de.timklge.karooreminder.screens import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,28 +13,34 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Done import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar 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 import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -42,6 +49,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat import com.maxkeppeker.sheets.core.models.base.UseCaseState import com.maxkeppeler.sheets.color.ColorDialog @@ -51,19 +59,31 @@ import com.maxkeppeler.sheets.color.models.ColorSelectionMode import com.maxkeppeler.sheets.color.models.MultipleColors import com.maxkeppeler.sheets.color.models.SingleColor import de.timklge.karooreminder.R +import io.hammerhead.karooext.KarooSystemService +import io.hammerhead.karooext.models.PlayBeepPattern +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedReminder: Reminder?) -> Unit, onCancel: () -> Unit){ + val ctx = LocalContext.current + val karooSystem = remember { KarooSystemService(ctx) } + LaunchedEffect(Unit) { + karooSystem.connect{} + } var title by remember { mutableStateOf(reminder.name) } var text by remember { mutableStateOf(reminder.text) } var selectedColor by remember { mutableIntStateOf(reminder.foregroundColor) } val colorDialogState by remember { mutableStateOf(UseCaseState()) } var duration by remember { mutableStateOf(reminder.interval.toString()) } var isActive by remember { mutableStateOf(reminder.isActive) } + var autoDismiss by remember { mutableStateOf(reminder.isAutoDismiss) } var deleteDialogVisible by remember { mutableStateOf(false) } + var toneDialogVisible by remember { mutableStateOf(false) } + var selectedTone by remember { mutableStateOf(reminder.tone) } - fun getUpdatedReminder(): Reminder = Reminder(reminder.id, title, duration.toIntOrNull() ?: 0, text, selectedColor, isActive) + fun getUpdatedReminder(): Reminder = Reminder(reminder.id, title, duration.toIntOrNull() ?: 0, + text, selectedColor, isActive, isAutoDismiss = autoDismiss, tone = selectedTone) Column(modifier = Modifier .fillMaxSize() @@ -116,11 +136,28 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi .shadow(5.dp, CircleShape) .width(40.dp), content = {}) - Spacer(modifier = Modifier.width(20.dp)) + Spacer(modifier = Modifier.width(5.dp)) Text("Change Color") } + FilledTonalButton(modifier = Modifier + .fillMaxWidth() + .height(60.dp), + onClick = { + toneDialogVisible = true + }) { + Icon(Icons.Default.Build, contentDescription = "Change Tone") + Spacer(modifier = Modifier.width(5.dp)) + Text("Change Tone") + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Switch(checked = autoDismiss, onCheckedChange = { autoDismiss = it}) + Spacer(modifier = Modifier.width(10.dp)) + Text("Auto-Dismiss") + } + Row(verticalAlignment = Alignment.CenterVertically) { Switch(checked = isActive, onCheckedChange = { isActive = it}) Spacer(modifier = Modifier.width(10.dp)) @@ -133,6 +170,7 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi onSubmit(getUpdatedReminder()) }) { Icon(Icons.Default.Done, contentDescription = "Save Reminder") + Spacer(modifier = Modifier.width(5.dp)) Text("Save") } @@ -142,6 +180,7 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi onCancel() }) { Icon(Icons.Default.Close, contentDescription = "Cancel Editing") + Spacer(modifier = Modifier.width(5.dp)) Text("Cancel") } @@ -169,6 +208,64 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi }, title = { Text("Delete reminder") }, text = { Text("Really delete this reminder?") }) } + + if (toneDialogVisible){ + Dialog(onDismissRequest = { toneDialogVisible = false }) { + var dialogSelectedTone by remember { mutableStateOf(selectedTone) } + val coroutineScope = rememberCoroutineScope() + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column(modifier = Modifier + .padding(5.dp) + .verticalScroll(rememberScrollState()) + .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) { + + ReminderBeepPattern.entries.forEach { pattern -> + Row(modifier = Modifier + .fillMaxWidth() + .clickable { + dialogSelectedTone = pattern + karooSystem.dispatch(PlayBeepPattern(pattern.tones)) + }, verticalAlignment = Alignment.CenterVertically) { + RadioButton(selected = dialogSelectedTone == pattern, onClick = { + dialogSelectedTone = pattern + karooSystem.dispatch(PlayBeepPattern(pattern.tones)) + }) + Text( + text = pattern.displayName, + modifier = Modifier.padding(start = 10.dp) + ) + } + } + + FilledTonalButton(modifier = Modifier + .fillMaxWidth() + .height(50.dp), onClick = { + selectedTone = dialogSelectedTone + toneDialogVisible = false + }) { + Icon(Icons.Default.Done, contentDescription = "Save") + Text("Save") + } + + FilledTonalButton(modifier = Modifier + .fillMaxWidth() + .height(50.dp), onClick = { + toneDialogVisible = false + }) { + Icon(Icons.Default.Close, contentDescription = "Cancel") + Text("Cancel") + } + + } + } + } + } } } } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooreminder/screens/MainScreen.kt b/app/src/main/kotlin/de/timklge/karooreminder/screens/MainScreen.kt index de20f02..91dfbcc 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/screens/MainScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/screens/MainScreen.kt @@ -1,6 +1,7 @@ package de.timklge.karooreminder.screens import android.content.Context +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -86,9 +87,15 @@ fun ReminderAppNavHost(modifier: Modifier = Modifier, navController: NavHostCont val ctx = LocalContext.current LaunchedEffect(Unit) { ctx.dataStore.data.distinctUntilChanged().collect { t -> - val entries = Json.decodeFromString>(t[preferencesKey] ?: defaultReminders) reminders.clear() - reminders.addAll(entries) + try { + val entries = Json.decodeFromString>( + t[preferencesKey] ?: defaultReminders + ) + reminders.addAll(entries) + } catch(e: Throwable){ + Log.e("karoo-reminder","Failed to read preferences", e) + } } } diff --git a/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt b/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt index e4ad443..8da8b51 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt @@ -3,14 +3,32 @@ package de.timklge.karooreminder.screens import android.content.Context import androidx.core.content.ContextCompat import de.timklge.karooreminder.R +import io.hammerhead.karooext.models.PlayBeepPattern import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +@Serializable +enum class ReminderBeepPattern(val displayName: String, val tones: List) { + NO_TONES("No tones", emptyList()), + THREE_TONES_UP("Three tones up", listOf(PlayBeepPattern.Tone(500, 200), PlayBeepPattern.Tone(700, 200), PlayBeepPattern.Tone(900, 200))), + THREE_TONES_DOWN("Three tones down", listOf(PlayBeepPattern.Tone(900, 200), PlayBeepPattern.Tone(700, 200), PlayBeepPattern.Tone(400, 200))), + DOUBLE_HIGH("Double high", listOf( + PlayBeepPattern.Tone(400, 400), + PlayBeepPattern.Tone(0, 200), + PlayBeepPattern.Tone(600, 200), + PlayBeepPattern.Tone(0, 200), + PlayBeepPattern.Tone(600, 200), + PlayBeepPattern.Tone(0, 200), + PlayBeepPattern.Tone(400, 400)) + ) +} + @Serializable class Reminder(val id: Int, var name: String, var interval: Int, var text: String, var foregroundColor: Int = android.graphics.Color.parseColor("#700000"), - val isActive: Boolean = true){ + val isActive: Boolean = true, val isAutoDismiss: Boolean = true, + val tone: ReminderBeepPattern = ReminderBeepPattern.THREE_TONES_UP){ fun getResourceColor(context: Context): Int { return when(foregroundColor){