Add beep tone select, auto dismiss option

This commit is contained in:
Tim Kluge 2024-12-01 10:42:39 +01:00
parent 769737c483
commit eeaf40d98a
4 changed files with 140 additions and 8 deletions

View File

@ -1,6 +1,8 @@
package de.timklge.karooreminder package de.timklge.karooreminder
import android.util.Log
import de.timklge.karooreminder.screens.Reminder import de.timklge.karooreminder.screens.Reminder
import de.timklge.karooreminder.screens.ReminderBeepPattern
import de.timklge.karooreminder.screens.defaultReminders import de.timklge.karooreminder.screens.defaultReminders
import de.timklge.karooreminder.screens.preferencesKey import de.timklge.karooreminder.screens.preferencesKey
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
@ -41,7 +43,14 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", "1.0") {
} }
val preferences = applicationContext.dataStore.data.map { remindersJson -> val preferences = applicationContext.dataStore.data.map { remindersJson ->
Json.decodeFromString<MutableList<Reminder>>(remindersJson[preferencesKey] ?: defaultReminders) try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e("karoo-reminder","Failed to read preferences", e)
mutableListOf()
}
} }
karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME) karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME)
@ -50,19 +59,20 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", "1.0") {
.distinctUntilChanged() .distinctUntilChanged()
.drop(1) .drop(1)
.combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders} .combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders}
.distinctUntilChanged { old, new -> old.first == new.first }
.collect { (elapsedMinutes, reminders) -> .collect { (elapsedMinutes, reminders) ->
reminders reminders
.filter { reminder -> reminder.isActive && elapsedMinutes % reminder.interval == 0 } .filter { reminder -> reminder.isActive && elapsedMinutes % reminder.interval == 0 }
.forEach { reminder -> .forEach { reminder ->
karooSystem.dispatch(TurnScreenOn) karooSystem.dispatch(TurnScreenOn)
delay(1_000) 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( karooSystem.dispatch(
InRideAlert( InRideAlert(
id = "reminder-${reminder.id}-${elapsedMinutes}", id = "reminder-${reminder.id}-${elapsedMinutes}",
detail = reminder.text, detail = reminder.text,
title = reminder.name, title = reminder.name,
autoDismissMs = 15_000, autoDismissMs = if(reminder.isAutoDismiss) 15_000 else null,
icon = R.drawable.ic_launcher, icon = R.drawable.ic_launcher,
textColor = R.color.white, textColor = R.color.white,
backgroundColor = reminder.getResourceColor(applicationContext) backgroundColor = reminder.getResourceColor(applicationContext)

View File

@ -1,6 +1,7 @@
package de.timklge.karooreminder.screens package de.timklge.karooreminder.screens
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.Close
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.maxkeppeker.sheets.core.models.base.UseCaseState import com.maxkeppeker.sheets.core.models.base.UseCaseState
import com.maxkeppeler.sheets.color.ColorDialog 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.MultipleColors
import com.maxkeppeler.sheets.color.models.SingleColor import com.maxkeppeler.sheets.color.models.SingleColor
import de.timklge.karooreminder.R import de.timklge.karooreminder.R
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.PlayBeepPattern
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedReminder: Reminder?) -> Unit, onCancel: () -> Unit){ 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 title by remember { mutableStateOf(reminder.name) }
var text by remember { mutableStateOf(reminder.text) } var text by remember { mutableStateOf(reminder.text) }
var selectedColor by remember { mutableIntStateOf(reminder.foregroundColor) } var selectedColor by remember { mutableIntStateOf(reminder.foregroundColor) }
val colorDialogState by remember { mutableStateOf(UseCaseState()) } val colorDialogState by remember { mutableStateOf(UseCaseState()) }
var duration by remember { mutableStateOf(reminder.interval.toString()) } var duration by remember { mutableStateOf(reminder.interval.toString()) }
var isActive by remember { mutableStateOf(reminder.isActive) } var isActive by remember { mutableStateOf(reminder.isActive) }
var autoDismiss by remember { mutableStateOf(reminder.isAutoDismiss) }
var deleteDialogVisible by remember { mutableStateOf(false) } 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 Column(modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -116,11 +136,28 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
.shadow(5.dp, CircleShape) .shadow(5.dp, CircleShape)
.width(40.dp), content = {}) .width(40.dp), content = {})
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(5.dp))
Text("Change Color") 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) { Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = isActive, onCheckedChange = { isActive = it}) Switch(checked = isActive, onCheckedChange = { isActive = it})
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
@ -133,6 +170,7 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
onSubmit(getUpdatedReminder()) onSubmit(getUpdatedReminder())
}) { }) {
Icon(Icons.Default.Done, contentDescription = "Save Reminder") Icon(Icons.Default.Done, contentDescription = "Save Reminder")
Spacer(modifier = Modifier.width(5.dp))
Text("Save") Text("Save")
} }
@ -142,6 +180,7 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
onCancel() onCancel()
}) { }) {
Icon(Icons.Default.Close, contentDescription = "Cancel Editing") Icon(Icons.Default.Close, contentDescription = "Cancel Editing")
Spacer(modifier = Modifier.width(5.dp))
Text("Cancel") Text("Cancel")
} }
@ -169,6 +208,64 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
}, },
title = { Text("Delete reminder") }, text = { Text("Really delete this reminder?") }) 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")
}
}
}
}
}
} }
} }
} }

View File

@ -1,6 +1,7 @@
package de.timklge.karooreminder.screens package de.timklge.karooreminder.screens
import android.content.Context import android.content.Context
import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -86,9 +87,15 @@ fun ReminderAppNavHost(modifier: Modifier = Modifier, navController: NavHostCont
val ctx = LocalContext.current val ctx = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
ctx.dataStore.data.distinctUntilChanged().collect { t -> ctx.dataStore.data.distinctUntilChanged().collect { t ->
val entries = Json.decodeFromString<MutableList<Reminder>>(t[preferencesKey] ?: defaultReminders)
reminders.clear() reminders.clear()
reminders.addAll(entries) try {
val entries = Json.decodeFromString<MutableList<Reminder>>(
t[preferencesKey] ?: defaultReminders
)
reminders.addAll(entries)
} catch(e: Throwable){
Log.e("karoo-reminder","Failed to read preferences", e)
}
} }
} }

View File

@ -3,14 +3,32 @@ package de.timklge.karooreminder.screens
import android.content.Context import android.content.Context
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.timklge.karooreminder.R import de.timklge.karooreminder.R
import io.hammerhead.karooext.models.PlayBeepPattern
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@Serializable
enum class ReminderBeepPattern(val displayName: String, val tones: List<PlayBeepPattern.Tone>) {
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 @Serializable
class Reminder(val id: Int, var name: String, var interval: Int, var text: String, class Reminder(val id: Int, var name: String, var interval: Int, var text: String,
var foregroundColor: Int = android.graphics.Color.parseColor("#700000"), 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 { fun getResourceColor(context: Context): Int {
return when(foregroundColor){ return when(foregroundColor){