From bf500596fbcbfdd6355bfb0dd49ab6aec2df9a4f Mon Sep 17 00:00:00 2001 From: Tim Kluge Date: Thu, 29 May 2025 13:15:24 +0200 Subject: [PATCH] Add profile selection dialog --- .../de/timklge/karooreminder/Extensions.kt | 12 +++ .../karooreminder/KarooReminderExtension.kt | 66 ++++++++------ .../karooreminder/screens/DetailScreen.kt | 91 ++++++++++++++++++- .../karooreminder/screens/MainScreen.kt | 5 +- .../timklge/karooreminder/screens/Reminder.kt | 13 ++- gradle/libs.versions.toml | 2 +- 6 files changed, 155 insertions(+), 34 deletions(-) diff --git a/app/src/main/kotlin/de/timklge/karooreminder/Extensions.kt b/app/src/main/kotlin/de/timklge/karooreminder/Extensions.kt index bbdd2b7..59246cc 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/Extensions.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/Extensions.kt @@ -7,6 +7,7 @@ import de.timklge.karooreminder.screens.Reminder import de.timklge.karooreminder.screens.defaultReminders import de.timklge.karooreminder.screens.preferencesKey import io.hammerhead.karooext.KarooSystemService +import io.hammerhead.karooext.models.ActiveRideProfile import io.hammerhead.karooext.models.OnStreamState import io.hammerhead.karooext.models.RideState import io.hammerhead.karooext.models.StreamState @@ -40,6 +41,17 @@ fun KarooSystemService.streamRideState(): Flow { } } +fun KarooSystemService.streamActiveRideProfile(): Flow { + return callbackFlow { + val listenerId = addConsumer { activeProfile: ActiveRideProfile -> + trySendBlocking(activeProfile) + } + awaitClose { + removeConsumer(listenerId) + } + } +} + fun KarooSystemService.streamUserProfile(): Flow { return callbackFlow { val listenerId = addConsumer { userProfile: UserProfile -> diff --git a/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt b/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt index 7371dbc..1371c32 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt @@ -5,8 +5,10 @@ import android.media.MediaPlayer import android.util.Log import de.timklge.karooreminder.screens.Reminder import de.timklge.karooreminder.screens.ReminderBeepPattern +import de.timklge.karooreminder.screens.reminderIsActive import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.extension.KarooExtension +import io.hammerhead.karooext.models.ActiveRideProfile import io.hammerhead.karooext.models.DataType import io.hammerhead.karooext.models.HardwareType import io.hammerhead.karooext.models.InRideAlert @@ -231,13 +233,19 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS val triggerJobs = mutableSetOf() + data class StreamData(val preferences: MutableList, val activeProfile: ActiveRideProfile) + + val streamDataFlow = combine(streamPreferences(), karooSystem.streamActiveRideProfile()) { reminders, activeProfile -> + StreamData(preferences = reminders, activeProfile = activeProfile) + } + triggerStreamJob = CoroutineScope(Dispatchers.IO).launch { - streamPreferences().collect { reminders -> + streamDataFlow.collect { (reminders, activeRideProfile) -> triggerJobs.forEach { it.cancel() } triggerJobs.clear() if (reminders.any { it.trigger == ReminderTrigger.DISTANCE }){ - val distanceJob = startIntervalJob(ReminderTrigger.DISTANCE) { + val distanceJob = startIntervalJob(reminders, activeRideProfile, ReminderTrigger.DISTANCE) { karooSystem.streamDataFlow(DataType.Type.DISTANCE) .mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile } @@ -255,7 +263,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS } if (reminders.any { it.trigger == ReminderTrigger.ELAPSED_TIME }){ - val elapsedTimeJob = startIntervalJob(ReminderTrigger.ELAPSED_TIME) { + val elapsedTimeJob = startIntervalJob(reminders, activeRideProfile, ReminderTrigger.ELAPSED_TIME) { karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME) .mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .map { (it / 1000 / 60).toInt() } @@ -266,7 +274,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS } if (reminders.any { it.trigger == ReminderTrigger.ENERGY_OUTPUT }){ - val energyOutputJob = startIntervalJob(ReminderTrigger.ENERGY_OUTPUT) { + val energyOutputJob = startIntervalJob(reminders, activeRideProfile, ReminderTrigger.ENERGY_OUTPUT) { karooSystem.streamDataFlow(DataType.Type.ENERGY_OUTPUT) .mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .map { it.toInt() } @@ -301,7 +309,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS intervalTriggers.forEach { trigger -> SmoothSetting.entries.forEach { smoothSetting -> if (reminders.any { it.trigger == trigger && it.smoothSetting == smoothSetting }){ - val job = startRangeExceededJob(trigger, smoothSetting) + val job = startRangeExceededJob(reminders, activeRideProfile, trigger, smoothSetting) triggerJobs.add(job) } } @@ -310,41 +318,43 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS } } - private fun startIntervalJob(trigger: ReminderTrigger, flow: () -> Flow): Job { + private fun startIntervalJob(preferences: List, activeRideProfile: ActiveRideProfile, trigger: ReminderTrigger, flow: () -> Flow): Job { return CoroutineScope(Dispatchers.IO).launch { - val preferences = streamPreferences() flow() .filterNot { it == 0 } - .combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders} - .distinctUntilChanged { old, new -> old.first == new.first } - .collectLatest { (elapsedMinutes, reminders) -> - val rs = reminders + .distinctUntilChanged() + .collectLatest { elapsedMinutes -> + val rs = preferences .filter { reminder -> val interval = reminder.interval - reminder.trigger == trigger && reminder.isActive && interval != null && elapsedMinutes % interval == 0 + reminder.trigger == trigger && reminderIsActive(reminder, activeRideProfile.profile) && interval != null && elapsedMinutes % interval == 0 } - for (reminder in rs){ + for (reminder in rs) { Log.i(TAG, "$trigger reminder: ${reminder.name}") - reminderChannel.send(DisplayedReminder(reminder.tone, trigger, InRideAlert( - id = "reminder-${reminder.id}-${elapsedMinutes}", - detail = reminder.text, - title = reminder.name, - autoDismissMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null, - icon = R.drawable.timer, - textColor = reminder.displayForegroundColor?.getTextColor() ?: R.color.black, - backgroundColor = reminder.displayForegroundColor?.colorRes ?: R.color.hRed - ))) + reminderChannel.send( + DisplayedReminder( + reminder.tone, trigger, InRideAlert( + id = "reminder-${reminder.id}-${elapsedMinutes}", + detail = reminder.text, + title = reminder.name, + autoDismissMs = if (reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null, + icon = R.drawable.timer, + textColor = reminder.displayForegroundColor?.getTextColor() + ?: R.color.black, + backgroundColor = reminder.displayForegroundColor?.colorRes + ?: R.color.hRed + ) + ) + ) } } } } - private fun startRangeExceededJob(triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job { + private fun startRangeExceededJob(preferences: MutableList, activeRideProfile: ActiveRideProfile, triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job { return CoroutineScope(Dispatchers.IO).launch { - val preferences = streamPreferences() - Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting") val valueStream = karooSystem.streamDataFlow(triggerType.getSmoothedDataType(smoothSetting)) @@ -370,9 +380,9 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS data class StreamData(val value: Double, val reminders: MutableList, val distanceImperial: Boolean, val temperatureImperial: Boolean, val rideState: RideState) - combine(valueStream, preferences, karooSystem.streamUserProfile(), karooSystem.streamRideState()) { value, reminders, profile, rideState -> + combine(valueStream, karooSystem.streamUserProfile(), karooSystem.streamRideState()) { value, profile, rideState -> StreamData(distanceImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, temperatureImperial = profile.preferredUnit.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL, - value = value, reminders = reminders, rideState = rideState) + value = value, reminders = preferences, rideState = rideState) }.filter { it.rideState is RideState.Recording }.let { @@ -432,7 +442,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType") } - reminder.isActive && reminder.trigger == triggerType && triggerIsMet + reminderIsActive(reminder, activeRideProfile.profile) && reminder.trigger == triggerType && triggerIsMet } triggered 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 efb2476..9b17107 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt @@ -103,6 +103,8 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi var selectedTone by remember { mutableStateOf(reminder.tone) } var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) } var selectedTrigger by remember { mutableStateOf(reminder.trigger) } + var rideProfileDialogVisible by remember { mutableStateOf(false) } + var enabledRideProfiles by remember { mutableStateOf(reminder.enabledRideProfiles.toMutableSet()) } val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null) @@ -116,7 +118,8 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi isActive = isActive, smoothSetting = smoothSetting, trigger = selectedTrigger, - isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15) + isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15, + enabledRideProfiles = enabledRideProfiles.toSet()) } Column(modifier = Modifier @@ -265,6 +268,16 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi Text("Is Active") } + FilledTonalButton(modifier = Modifier + .fillMaxWidth() + .height(60.dp), onClick = { + rideProfileDialogVisible = true + }) { + Icon(Icons.Default.Build, contentDescription = "Change Ride Profiles", modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.width(5.dp)) + Text("Ride Profiles: ${if (enabledRideProfiles.isEmpty()) "All" else enabledRideProfiles.joinToString(", ")}") + } + FilledTonalButton(modifier = Modifier .fillMaxWidth() .height(50.dp), onClick = { @@ -310,6 +323,79 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi title = { Text("Delete reminder") }, text = { Text("Really delete this reminder?") }) } + if (rideProfileDialogVisible) { + var newProfileName by remember { mutableStateOf("") } + Dialog(onDismissRequest = { rideProfileDialogVisible = false }) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + shape = RoundedCornerShape(10.dp), + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (enabledRideProfiles.isEmpty()) { + Text("All profiles enabled") + } else { + Column(modifier = Modifier.fillMaxWidth()) { + enabledRideProfiles.forEach { profileName -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(profileName) + Button(onClick = { + enabledRideProfiles = enabledRideProfiles.toMutableSet().apply { remove(profileName) } + }) { + Icon(Icons.Default.Delete, contentDescription = "Delete profile") + } + } + } + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + OutlinedTextField( + value = newProfileName, + onValueChange = { newProfileName = it }, + label = { Text("New profile name") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + Button( + onClick = { + if (newProfileName.isNotBlank()) { + enabledRideProfiles = enabledRideProfiles.toMutableSet().apply { add(newProfileName) } + newProfileName = "" // Clear the text field + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Add Profile") + } + + Spacer(modifier = Modifier.height(10.dp)) + + Button( + onClick = { rideProfileDialogVisible = false }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Close") + } + } + } + } + } + if (triggerDialogVisible){ Dialog(onDismissRequest = { triggerDialogVisible = false }) { Card( @@ -429,4 +515,5 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi } } } -} \ 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 0b3490e..6833adb 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/screens/MainScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/screens/MainScreen.kt @@ -63,6 +63,8 @@ import androidx.navigation.navArgument import de.timklge.karooreminder.KarooReminderExtension import de.timklge.karooreminder.R import de.timklge.karooreminder.dataStore +import de.timklge.karooreminder.streamActiveRideProfile +import de.timklge.karooreminder.streamRideState import de.timklge.karooreminder.streamUserProfile import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.models.UserProfile @@ -165,6 +167,7 @@ fun MainScreen(reminders: MutableList, onNavigateToReminder: (r: Remin var showWarnings by remember { mutableStateOf(false) } val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null) + val currentRideProfile by karooSystem.streamActiveRideProfile().collectAsStateWithLifecycle(null) LaunchedEffect(Unit) { delay(1000L) @@ -197,7 +200,7 @@ fun MainScreen(reminders: MutableList, onNavigateToReminder: (r: Remin Card(Modifier .fillMaxWidth() .height(60.dp) - .alpha(if (reminder.isActive) 1f else 0.6f) + .alpha(if (reminderIsActive(reminder, currentRideProfile?.profile)) 1f else 0.6f) .clickable { onNavigateToReminder(reminder) } .padding(5.dp), shape = RoundedCornerShape(corner = CornerSize(10.dp)) ) { 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 53bc565..b4abfdf 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt @@ -7,6 +7,7 @@ import de.timklge.karooreminder.R import de.timklge.karooreminder.ReminderTrigger import de.timklge.karooreminder.SmoothSetting import io.hammerhead.karooext.models.PlayBeepPattern +import io.hammerhead.karooext.models.RideProfile import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -141,6 +142,14 @@ class Reminder(val id: Int, var name: String, 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) + val autoDismissSeconds: Int = 15, + val enabledRideProfiles: Set = emptySet()) -val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, text = "Take a sip!"))) \ No newline at end of file +val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, text = "Take a sip!"))) + +fun reminderIsActive(reminder: Reminder, currentRideProfile: RideProfile?): Boolean { + val enabledRideProfiles = reminder.enabledRideProfiles.map { it.lowercase().trim() } + val currentProfileName = currentRideProfile?.name?.lowercase()?.trim() + + return reminder.isActive && (currentRideProfile == null || reminder.enabledRideProfiles.isEmpty() || enabledRideProfiles.contains(currentProfileName)) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dddecba..261486c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = " [libraries] androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } color = { module = "com.maxkeppeler.sheets-compose-dialogs:color", version.ref = "color" } -hammerhead-karoo-ext = { group = "io.hammerhead", name = "karoo-ext", version = "1.1.1" } +hammerhead-karoo-ext = { group = "io.hammerhead", name = "karoo-ext", version = "1.1.5" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }