Add distance, hr / power / speed / cadence range triggers and light color set (#12)

* Add distance, range triggers

* Update version

* Brighten colors

* Add speed, cadence range triggers, fix imperial units

* Only trigger range alerts if more than 5 values have been received within the last 10 seconds

* Update README

* Rename icon license file
This commit is contained in:
timklge 2025-01-01 19:05:16 +01:00 committed by GitHub
parent ee15466517
commit 19249e640e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 447 additions and 44 deletions

View File

@ -4,7 +4,7 @@
![GitHub Downloads (specific asset, all releases)](https://img.shields.io/github/downloads/timklge/karoo-reminder/app-release.apk)
![GitHub License](https://img.shields.io/github/license/timklge/karoo-reminder)
Basic karoo extension that shows in-ride alerts every X minutes. For Karoo 2 and Karoo 3 devices.
Karoo extension that displays in-ride alerts based on custom triggers. Reminders can be set to activate after a specific time interval, distance traveled, or when a sensor value is outside a defined range (e.g., heart rate exceeds zone 2).
![Reminder List](list.png)
![Reminder Detail](detail.png)

View File

@ -15,8 +15,8 @@ android {
applicationId = "de.timklge.karooreminder"
minSdk = 26
targetSdk = 34
versionCode = 8
versionName = "1.0.7"
versionCode = 9
versionName = "1.1"
}
signingConfigs {

View File

@ -3,9 +3,9 @@
"packageName": "de.timklge.karooreminder",
"iconUrl": "https://github.com/timklge/karoo-reminder/releases/latest/download/karoo-reminder.png",
"latestApkUrl": "https://github.com/timklge/karoo-reminder/releases/latest/download/app-release.apk",
"latestVersion": "1.0.7",
"latestVersionCode": 8,
"latestVersion": "1.1",
"latestVersionCode": 9,
"developer": "timklge",
"description": "Simple karoo extension that shows in-ride alerts every X minutes",
"releaseNotes": "Added display duration setting, bluetooth alert sound. Built via Github CD"
"description": "Shows in-ride alerts after a given time interval, distance or HR / power / speed / cadence out of range",
"releaseNotes": "Added distance, HR / power out of range trigger types"
}

View File

@ -0,0 +1,60 @@
package de.timklge.karooreminder
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
data class DropdownOption(val id: String, val name: String)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Dropdown(label: String, options: List<DropdownOption>, selected: DropdownOption, onSelect: (selectedOption: DropdownOption) -> Unit) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
) {
OutlinedTextField(
readOnly = true,
value = selected.name,
onValueChange = { },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable, true).fillMaxWidth(),
label = { Text(label) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(option.name, style = MaterialTheme.typography.bodyLarge) },
onClick = {
expanded = false
onSelect(option)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}

View File

@ -4,6 +4,7 @@ import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.OnStreamState
import io.hammerhead.karooext.models.RideState
import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
@ -30,3 +31,14 @@ fun KarooSystemService.streamRideState(): Flow<RideState> {
}
}
}
fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
return callbackFlow {
val listenerId = addConsumer { userProfile: UserProfile ->
trySendBlocking(userProfile)
}
awaitClose {
removeConsumer(listenerId)
}
}
}

View File

@ -14,21 +14,58 @@ import io.hammerhead.karooext.models.InRideAlert
import io.hammerhead.karooext.models.PlayBeepPattern
import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.TurnScreenOn
import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
class KarooReminderExtension : KarooExtension("karoo-reminder", "1.0.7") {
enum class ReminderTrigger(val id: String, val label: String) {
ELAPSED_TIME("elapsed_time", "Elapsed Time"),
DISTANCE("distance", "Distance"),
HR_LIMIT_MAXIMUM_EXCEEDED("hr_limit_max", "HR above value"),
HR_LIMIT_MINIMUM_EXCEEDED("hr_limit_min", "HR below value"),
POWER_LIMIT_MAXIMUM_EXCEEDED("power_limit_min", "Power above value"),
POWER_LIMIT_MINIMUM_EXCEEDED("power_limit_min", "Power below value"),
SPEED_LIMIT_MAXIMUM_EXCEEDED("speed_limit_max", "Speed above value"),
SPEED_LIMIT_MINIMUM_EXCEEDED("speed_limit_min", "Speed below value"),
CADENCE_LIMIT_MAXIMUM_EXCEEDED("cadence_limit_max", "Cadence above value"),
CADENCE_LIMIT_MINIMUM_EXCEEDED("cadence_limit_min", "Cadence below value");
fun getPrefix(): String {
return when (this) {
HR_LIMIT_MINIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED -> "<"
HR_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MAXIMUM_EXCEEDED -> ">"
ELAPSED_TIME, DISTANCE -> ""
}
}
fun getSuffix(imperial: Boolean): String {
return when (this) {
ELAPSED_TIME -> "min"
DISTANCE -> if(imperial) "mi" else "km"
HR_LIMIT_MAXIMUM_EXCEEDED, HR_LIMIT_MINIMUM_EXCEEDED -> "bpm"
POWER_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED -> "W"
SPEED_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED -> if(imperial) "mph" else "km/h"
CADENCE_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED -> "rpm"
}
}
}
class KarooReminderExtension : KarooExtension("karoo-reminder", "1.1") {
companion object {
const val TAG = "karoo-reminder"
@ -36,22 +73,125 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", "1.0.7") {
private lateinit var karooSystem: KarooSystemService
private var serviceJob: Job? = null
private var jobs: MutableSet<Job> = mutableSetOf()
data class DisplayedReminder(val tones: ReminderBeepPattern, val trigger: ReminderTrigger, val alert: InRideAlert)
private var reminderChannel = Channel<DisplayedReminder>(2, BufferOverflow.DROP_OLDEST)
private var mediaPlayer: MediaPlayer? = null
private suspend fun receiverWorker() {
for(displayedReminder in reminderChannel) {
Log.i(TAG, "Dispatching reminder: ${displayedReminder.alert.title}")
try {
karooSystem.dispatch(TurnScreenOn)
val isAutoDismiss = displayedReminder.alert.autoDismissMs != null
val autoDismissMs = (displayedReminder.alert.autoDismissMs ?: 0L)
val intent = Intent("de.timklge.HIDE_POWERBAR").apply {
putExtra("duration", (if (isAutoDismiss) autoDismissMs else 15_000L) + 1000L)
putExtra("location", "top")
}
delay(1_000)
applicationContext.sendBroadcast(intent)
if (displayedReminder.tones != ReminderBeepPattern.NO_TONES) {
karooSystem.dispatch(PlayBeepPattern(displayedReminder.tones.tones))
mediaPlayer?.start()
}
karooSystem.dispatch(displayedReminder.alert)
val delayMs = if (isAutoDismiss) autoDismissMs else 20000L
delay(delayMs)
} catch(e: ClosedReceiveChannelException){
Log.w(TAG, "Dispatch channel closed, exiting")
return
} catch(e: Exception){
Log.e(TAG, "Failed to dispatch reminder", e)
}
}
}
override fun onCreate() {
super.onCreate()
mediaPlayer = MediaPlayer.create(this, R.raw.reminder)
karooSystem = KarooSystemService(applicationContext)
val mediaPlayer = MediaPlayer.create(this, R.raw.reminder)
val receiveJob = CoroutineScope(Dispatchers.IO).launch {
receiverWorker()
}
jobs.add(receiveJob)
serviceJob = CoroutineScope(Dispatchers.IO).launch {
karooSystem.connect { connected ->
if (connected) {
Log.i(TAG, "Connected")
karooSystem.connect { connected ->
if (connected) {
Log.i(TAG, "Connected")
}
}
val distanceJob = CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e(TAG,"Failed to read preferences", e)
mutableListOf()
}
}
karooSystem.streamDataFlow(DataType.Type.DISTANCE)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile }
.map { (distance, profile) ->
if (profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL){
(distance / 1609.344).toInt()
} else {
(distance / 1000.0).toInt()
}
}
.distinctUntilChanged()
.filterNot { it == 0 }
.combine(preferences) { distance, reminders -> distance to reminders}
.distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (distance, reminders) ->
val rs = reminders
.filter { reminder -> reminder.trigger == ReminderTrigger.DISTANCE && reminder.isActive && distance % reminder.interval == 0 }
for (reminder in rs){
Log.i(TAG, "Distance reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.DISTANCE, InRideAlert(
id = "reminder-${reminder.id}-${distance}",
detail = reminder.text,
title = reminder.name,
autoDismissMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
icon = R.drawable.ic_launcher,
textColor = reminder.getTextColor(applicationContext),
backgroundColor = reminder.getResourceColor(applicationContext)
)))
}
}
}
jobs.add(distanceJob)
jobs.addAll(listOf(
startRangeExceededJob(ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED)
))
val elapsedTimeJob = CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
@ -72,45 +212,108 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", "1.0.7") {
.distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (elapsedMinutes, reminders) ->
val rs = reminders
.filter { reminder -> reminder.isActive && elapsedMinutes % reminder.interval == 0 }
.filter { reminder -> reminder.trigger == ReminderTrigger.ELAPSED_TIME && reminder.isActive && elapsedMinutes % reminder.interval == 0 }
for (reminder in rs){
karooSystem.dispatch(TurnScreenOn)
Log.i(TAG, "Elapsed time reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.ELAPSED_TIME, InRideAlert(
id = "reminder-${reminder.id}-${elapsedMinutes}",
detail = reminder.text,
title = reminder.name,
autoDismissMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
icon = R.drawable.ic_launcher,
textColor = reminder.getTextColor(applicationContext),
backgroundColor = reminder.getResourceColor(applicationContext)
)))
}
}
}
jobs.add(elapsedTimeJob)
}
val intent = Intent("de.timklge.HIDE_POWERBAR").apply {
putExtra("duration", (if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else 15_000L) + 1000L)
putExtra("location", "top")
data class StreamData(val value: Double, val reminders: MutableList<Reminder>? = null, val imperial: Boolean = false)
private fun startRangeExceededJob(triggerType: ReminderTrigger): Job {
return CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch (e: Throwable) {
Log.e(TAG, "Failed to read preferences", e)
mutableListOf()
}
}
val dataType = when (triggerType) {
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.HEART_RATE
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_POWER
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_SPEED
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE
ReminderTrigger.DISTANCE, ReminderTrigger.ELAPSED_TIME -> error("Unsupported trigger type: $triggerType")
}
karooSystem.streamDataFlow(dataType)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.filter { it > 0.0 }
.combine(preferences) { value, reminders -> StreamData(value, reminders) }
.combine(karooSystem.streamUserProfile()) { streamData, profile -> streamData.copy(imperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) }
.onlyIfNValuesReceivedWithinTimeframe(5, 1000 * 10) // At least 5 values have been received over the last 10 seconds
.map { (value, reminders, imperial) ->
val triggered = reminders?.filter { reminder ->
val isSpeedTrigger = triggerType == ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED || triggerType == ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED
val reminderValue = if (isSpeedTrigger){
// Convert m/s speed to km/h or mph
if (imperial) reminder.interval * 0.44704 else reminder.interval * 0.277778
} else {
reminder.interval.toDouble()
}
delay(1_000)
applicationContext.sendBroadcast(intent)
val triggerIsMet = when (triggerType){
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED -> value > reminderValue
if (reminder.tone != ReminderBeepPattern.NO_TONES){
karooSystem.dispatch(PlayBeepPattern(reminder.tone.tones))
mediaPlayer.start()
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> value < reminderValue
ReminderTrigger.DISTANCE, ReminderTrigger.ELAPSED_TIME -> error("Unsupported trigger type: $triggerType")
}
karooSystem.dispatch(
InRideAlert(
id = "reminder-${reminder.id}-${elapsedMinutes}",
detail = reminder.text,
title = reminder.name,
autoDismissMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
icon = R.drawable.ic_launcher,
textColor = R.color.white,
backgroundColor = reminder.getResourceColor(applicationContext)
),
reminder.isActive && reminder.trigger == triggerType && triggerIsMet
}
triggered
}
.filterNotNull()
.filter { it.isNotEmpty() }
.throttle(1_000 * 60) // At most once every minute
.collectLatest { reminders ->
Log.i(TAG, "Triggered range reminder: ${reminders.size} reminders")
reminders.forEach { reminder ->
reminderChannel.send(
DisplayedReminder(
reminder.tone, triggerType, InRideAlert(
id = "reminder-${reminder.id}-${System.currentTimeMillis()}",
detail = reminder.text,
title = reminder.name,
autoDismissMs = if (reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
icon = R.drawable.ic_launcher,
textColor = reminder.getTextColor(applicationContext),
backgroundColor = reminder.getResourceColor(applicationContext)
)
)
)
val delayMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else 20000L
delay(delayMs)
}
}
}
}
override fun onDestroy() {
serviceJob?.cancel()
serviceJob = null
jobs.forEach { job -> job.cancel() }
jobs.clear()
karooSystem.disconnect()
super.onDestroy()
}

View File

@ -0,0 +1,31 @@
package de.timklge.karooreminder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
fun<T> Flow<T>.throttle(timeout: Long): Flow<T> = flow {
var lastEmissionTime = 0L
collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime >= timeout) {
emit(value)
lastEmissionTime = currentTime
}
}
}
fun<T> Flow<T>.onlyIfNValuesReceivedWithinTimeframe(n: Int, timeframe: Long): Flow<T> = flow {
val lastValuesReceivedAt = mutableListOf<Long>()
collect { value ->
val currentTime = System.currentTimeMillis()
lastValuesReceivedAt.removeAll { it + timeframe < currentTime }
lastValuesReceivedAt.add(currentTime)
if (lastValuesReceivedAt.size >= n) {
emit(value)
}
}
}

View File

@ -50,6 +50,7 @@ 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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.maxkeppeker.sheets.core.models.base.UseCaseState
import com.maxkeppeler.sheets.color.ColorDialog
import com.maxkeppeler.sheets.color.models.ColorConfig
@ -57,9 +58,14 @@ import com.maxkeppeler.sheets.color.models.ColorSelection
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.Dropdown
import de.timklge.karooreminder.DropdownOption
import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.PlayBeepPattern
import io.hammerhead.karooext.models.UserProfile
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -80,9 +86,16 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
var toneDialogVisible by remember { mutableStateOf(false) }
var selectedTone by remember { mutableStateOf(reminder.tone) }
var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) }
var selectedTrigger by remember { mutableStateOf(reminder.trigger) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
fun getUpdatedReminder(): Reminder = Reminder(reminder.id, title, duration.toIntOrNull() ?: 1,
text, selectedColor, isActive, isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
text = text,
foregroundColor = selectedColor,
isActive = isActive,
trigger = selectedTrigger,
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
Column(modifier = Modifier
.fillMaxSize()
@ -96,10 +109,51 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
OutlinedTextField(value = text, onValueChange = { text = it }, label = { Text("Text") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
apply {
val dropdownOptions = ReminderTrigger.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val dropdownInitialSelection by remember(selectedTrigger) {
mutableStateOf(dropdownOptions.find { option -> option.id == selectedTrigger.id }!!)
}
Dropdown(label = "Trigger", options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption ->
val previousTrigger = selectedTrigger
selectedTrigger = ReminderTrigger.entries.find { entry -> entry.id == selectedOption.id }!!
if (selectedTrigger != previousTrigger) {
duration = when (selectedTrigger) {
ReminderTrigger.ELAPSED_TIME -> 30.toString()
ReminderTrigger.DISTANCE -> 10.toString()
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED -> 160.toString()
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED -> 200.toString()
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED -> 60.toString()
ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED -> 100.toString()
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED -> 40.toString()
ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> 20.toString()
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED -> 120.toString()
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> 60.toString()
}
}
}
}
OutlinedTextField(value = duration, modifier = Modifier.fillMaxWidth(),
onValueChange = { duration = it },
label = { Text("Interval") },
suffix = { Text("min") },
label = {
when(selectedTrigger){
ReminderTrigger.ELAPSED_TIME -> Text("Interval")
ReminderTrigger.DISTANCE -> Text("Distance")
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum heart rate")
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum power")
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum heart rate")
ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum power")
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum speed")
ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum speed")
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum cadence")
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum cadence")
}
},
suffix = {
Text(selectedTrigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL))
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true
)
@ -120,6 +174,13 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
ContextCompat.getColor(LocalContext.current, R.color.bGreen),
ContextCompat.getColor(LocalContext.current, R.color.bBlue),
ContextCompat.getColor(LocalContext.current, R.color.bCyan),
ContextCompat.getColor(LocalContext.current, R.color.hRed),
ContextCompat.getColor(LocalContext.current, R.color.hPurple),
ContextCompat.getColor(LocalContext.current, R.color.hYellow),
ContextCompat.getColor(LocalContext.current, R.color.hGreen),
ContextCompat.getColor(LocalContext.current, R.color.hBlue),
ContextCompat.getColor(LocalContext.current, R.color.hCyan),
),
)
)

View File

@ -47,6 +47,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColor
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
@ -55,7 +56,9 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import de.timklge.karooreminder.KarooReminderExtension
import de.timklge.karooreminder.dataStore
import de.timklge.karooreminder.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@ -153,6 +156,8 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
val karooSystem = remember { KarooSystemService(ctx) }
var showWarnings by remember { mutableStateOf(false) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
LaunchedEffect(Unit) {
delay(1000L)
showWarnings = true
@ -199,7 +204,7 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
Spacer(Modifier.weight(1.0f))
Text("${reminder.interval}min")
Text("${reminder.trigger.getPrefix()}${reminder.interval}${reminder.trigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)}")
}
}
}

View File

@ -3,6 +3,7 @@ package de.timklge.karooreminder.screens
import android.content.Context
import androidx.core.content.ContextCompat
import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger
import io.hammerhead.karooext.models.PlayBeepPattern
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@ -29,8 +30,21 @@ class Reminder(val id: Int, var name: String, var interval: Int, var text: Strin
var foregroundColor: Int = android.graphics.Color.parseColor("#700000"),
val isActive: Boolean = true, val isAutoDismiss: Boolean = true,
val tone: ReminderBeepPattern = ReminderBeepPattern.THREE_TONES_UP,
var trigger: ReminderTrigger = ReminderTrigger.ELAPSED_TIME,
val autoDismissSeconds: Int = 15){
fun getTextColor(context: Context): Int {
return when(foregroundColor){
ContextCompat.getColor(context, R.color.bRed),
ContextCompat.getColor(context, R.color.bPurple),
ContextCompat.getColor(context, R.color.bYellow),
ContextCompat.getColor(context, R.color.bGreen),
ContextCompat.getColor(context, R.color.bBlue),
ContextCompat.getColor(context, R.color.bCyan) -> R.color.white
else -> R.color.black
}
}
fun getResourceColor(context: Context): Int {
return when(foregroundColor){
ContextCompat.getColor(context, R.color.bRed) -> R.color.bRed
@ -39,6 +53,14 @@ class Reminder(val id: Int, var name: String, var interval: Int, var text: Strin
ContextCompat.getColor(context, R.color.bGreen) -> R.color.bGreen
ContextCompat.getColor(context, R.color.bBlue) -> R.color.bBlue
ContextCompat.getColor(context, R.color.bCyan) -> R.color.bCyan
ContextCompat.getColor(context, R.color.hRed) -> R.color.hRed
ContextCompat.getColor(context, R.color.hPurple) -> R.color.hPurple
ContextCompat.getColor(context, R.color.hYellow) -> R.color.hYellow
ContextCompat.getColor(context, R.color.hGreen) -> R.color.hGreen
ContextCompat.getColor(context, R.color.hBlue) -> R.color.hBlue
ContextCompat.getColor(context, R.color.hCyan) -> R.color.hCyan
else -> error("Unknown color")
}
}

View File

@ -4,10 +4,19 @@
<color name="colorPrimaryDark">#2600B3</color>
<color name="white">#ffffff</color>
<color name="black">#000000</color>
<color name="bRed">#700000</color>
<color name="bGreen">#007000</color>
<color name="bBlue">#000070</color>
<color name="bPurple">#700070</color>
<color name="bYellow">#707000</color>
<color name="bCyan">#007070</color>
<color name="hRed">#FF6060</color>
<color name="hGreen">#50FF50</color>
<color name="hBlue">#7070FF</color>
<color name="hPurple">#FF70FF</color>
<color name="hYellow">#FFFF60</color>
<color name="hCyan">#60FFFF</color>
</resources>