Add profile selection dialog (#54)
All checks were successful
Build / build (push) Successful in 4m38s

* Setup BASE_URL ci config

* Add profile selection dialog

* Update changelog

* Enable pipeline for all branches

* Fix BASE_URL

* Add readme info

* Update button styling

* Only use profile selection dialog to input new profiles

* Update button colors
This commit is contained in:
timklge 2025-05-29 19:56:54 +02:00 committed by GitHub
parent e68d4fabca
commit bf8a588020
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 173 additions and 46 deletions

View File

@ -3,10 +3,10 @@ name: Build
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ "master" ] branches: [ "**" ]
tags: [ "*" ] tags: [ "*" ]
pull_request: pull_request:
branches: [ "master" ] branches: [ "**" ]
jobs: jobs:
build: build:
@ -26,7 +26,7 @@ jobs:
echo "KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV echo "KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
echo "KEYSTORE_BASE64=${{ secrets.KEYSTORE_BASE64 }}" >> $GITHUB_ENV echo "KEYSTORE_BASE64=${{ secrets.KEYSTORE_BASE64 }}" >> $GITHUB_ENV
echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV
echo "BASE_URL=${{ secrets.BASE_URL || '"https://github.com/timklge/karoo-powerbar/releases/latest/download"' }}" >> $GITHUB_ENV echo "BASE_URL=${{ secrets.BASE_URL || 'https://github.com/timklge/karoo-reminder/releases/latest/download' }}" >> $GITHUB_ENV
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: set up JDK 17 - name: set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@ -61,3 +61,4 @@ jobs:
list.png list.png
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -6,6 +6,8 @@
Karoo extension that displays in-ride alerts based on custom triggers. Reminders can be set to activate after a specific time interval, distance traveled, energy output or when a sensor value is outside a defined range (e.g., heart rate exceeds zone 2). Karoo extension that displays in-ride alerts based on custom triggers. Reminders can be set to activate after a specific time interval, distance traveled, energy output or when a sensor value is outside a defined range (e.g., heart rate exceeds zone 2).
By default, created reminders are active for all ride profiles. If you want to limit reminders to specific ride profiles (e. g. your "Gravel" profile), you can do so in the reminder settings.
Compatible with Karoo 2 and Karoo 3 devices. Compatible with Karoo 2 and Karoo 3 devices.
![Reminder List](list.png) ![Reminder List](list.png)

View File

@ -73,7 +73,7 @@ tasks.register("generateManifest") {
"latestVersionCode" to android.defaultConfig.versionCode, "latestVersionCode" to android.defaultConfig.versionCode,
"developer" to "github.com/timklge", "developer" to "github.com/timklge",
"description" to "Open-source extension that shows in-ride alerts after a given time interval has passed, distance has been traveled or HR / power / speed / cadence range is exceeded", "description" to "Open-source extension that shows in-ride alerts after a given time interval has passed, distance has been traveled or HR / power / speed / cadence range is exceeded",
"releaseNotes" to "* Add rolling average setting for power triggers", "releaseNotes" to "* Add profile selection dialog per reminder\n* Add rolling average setting for power triggers",
"screenshotUrls" to listOf( "screenshotUrls" to listOf(
"$baseUrl/reminder.png", "$baseUrl/reminder.png",
"$baseUrl/list.png", "$baseUrl/list.png",

View File

@ -7,6 +7,7 @@ import de.timklge.karooreminder.screens.Reminder
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
import io.hammerhead.karooext.models.ActiveRideProfile
import io.hammerhead.karooext.models.OnStreamState import io.hammerhead.karooext.models.OnStreamState
import io.hammerhead.karooext.models.RideState import io.hammerhead.karooext.models.RideState
import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.StreamState
@ -40,6 +41,17 @@ fun KarooSystemService.streamRideState(): Flow<RideState> {
} }
} }
fun KarooSystemService.streamActiveRideProfile(): Flow<ActiveRideProfile> {
return callbackFlow {
val listenerId = addConsumer { activeProfile: ActiveRideProfile ->
trySendBlocking(activeProfile)
}
awaitClose {
removeConsumer(listenerId)
}
}
}
fun KarooSystemService.streamUserProfile(): Flow<UserProfile> { fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
return callbackFlow { return callbackFlow {
val listenerId = addConsumer { userProfile: UserProfile -> val listenerId = addConsumer { userProfile: UserProfile ->

View File

@ -5,8 +5,10 @@ import android.media.MediaPlayer
import android.util.Log 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.ReminderBeepPattern
import de.timklge.karooreminder.screens.reminderIsActive
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.KarooExtension import io.hammerhead.karooext.extension.KarooExtension
import io.hammerhead.karooext.models.ActiveRideProfile
import io.hammerhead.karooext.models.DataType import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.HardwareType import io.hammerhead.karooext.models.HardwareType
import io.hammerhead.karooext.models.InRideAlert import io.hammerhead.karooext.models.InRideAlert
@ -231,13 +233,19 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
val triggerJobs = mutableSetOf<Job>() val triggerJobs = mutableSetOf<Job>()
data class StreamData(val preferences: MutableList<Reminder>, val activeProfile: ActiveRideProfile)
val streamDataFlow = combine(streamPreferences(), karooSystem.streamActiveRideProfile()) { reminders, activeProfile ->
StreamData(preferences = reminders, activeProfile = activeProfile)
}
triggerStreamJob = CoroutineScope(Dispatchers.IO).launch { triggerStreamJob = CoroutineScope(Dispatchers.IO).launch {
streamPreferences().collect { reminders -> streamDataFlow.collect { (reminders, activeRideProfile) ->
triggerJobs.forEach { it.cancel() } triggerJobs.forEach { it.cancel() }
triggerJobs.clear() triggerJobs.clear()
if (reminders.any { it.trigger == ReminderTrigger.DISTANCE }){ if (reminders.any { it.trigger == ReminderTrigger.DISTANCE }){
val distanceJob = startIntervalJob(ReminderTrigger.DISTANCE) { val distanceJob = startIntervalJob(reminders, activeRideProfile, ReminderTrigger.DISTANCE) {
karooSystem.streamDataFlow(DataType.Type.DISTANCE) karooSystem.streamDataFlow(DataType.Type.DISTANCE)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile } .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 }){ 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) karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { (it / 1000 / 60).toInt() } .map { (it / 1000 / 60).toInt() }
@ -266,7 +274,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
} }
if (reminders.any { it.trigger == ReminderTrigger.ENERGY_OUTPUT }){ 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) karooSystem.streamDataFlow(DataType.Type.ENERGY_OUTPUT)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { it.toInt() } .map { it.toInt() }
@ -301,7 +309,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
intervalTriggers.forEach { trigger -> intervalTriggers.forEach { trigger ->
SmoothSetting.entries.forEach { smoothSetting -> SmoothSetting.entries.forEach { smoothSetting ->
if (reminders.any { it.trigger == trigger && it.smoothSetting == 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) triggerJobs.add(job)
} }
} }
@ -310,41 +318,43 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
} }
} }
private fun startIntervalJob(trigger: ReminderTrigger, flow: () -> Flow<Int>): Job { private fun startIntervalJob(preferences: List<Reminder>, activeRideProfile: ActiveRideProfile, trigger: ReminderTrigger, flow: () -> Flow<Int>): Job {
return CoroutineScope(Dispatchers.IO).launch { return CoroutineScope(Dispatchers.IO).launch {
val preferences = streamPreferences()
flow() flow()
.filterNot { it == 0 } .filterNot { it == 0 }
.combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders} .distinctUntilChanged()
.distinctUntilChanged { old, new -> old.first == new.first } .collectLatest { elapsedMinutes ->
.collectLatest { (elapsedMinutes, reminders) -> val rs = preferences
val rs = reminders
.filter { reminder -> .filter { reminder ->
val interval = reminder.interval 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}") Log.i(TAG, "$trigger reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, trigger, InRideAlert( reminderChannel.send(
id = "reminder-${reminder.id}-${elapsedMinutes}", DisplayedReminder(
detail = reminder.text, reminder.tone, trigger, InRideAlert(
title = reminder.name, id = "reminder-${reminder.id}-${elapsedMinutes}",
autoDismissMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null, detail = reminder.text,
icon = R.drawable.timer, title = reminder.name,
textColor = reminder.displayForegroundColor?.getTextColor() ?: R.color.black, autoDismissMs = if (reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
backgroundColor = reminder.displayForegroundColor?.colorRes ?: R.color.hRed 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<Reminder>, activeRideProfile: ActiveRideProfile, triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job {
return CoroutineScope(Dispatchers.IO).launch { return CoroutineScope(Dispatchers.IO).launch {
val preferences = streamPreferences()
Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting") Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting")
val valueStream = karooSystem.streamDataFlow(triggerType.getSmoothedDataType(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<Reminder>, val distanceImperial: Boolean, val temperatureImperial: Boolean, val rideState: RideState) data class StreamData(val value: Double, val reminders: MutableList<Reminder>, 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, 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 { }.filter {
it.rideState is RideState.Recording it.rideState is RideState.Recording
}.let { }.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") 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 triggered

View File

@ -18,6 +18,7 @@ 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.Add
import androidx.compose.material.icons.filled.Build 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
@ -25,6 +26,7 @@ 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.Card
import androidx.compose.material3.CardDefaults
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
@ -103,6 +105,8 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
var selectedTone by remember { mutableStateOf(reminder.tone) } var selectedTone by remember { mutableStateOf(reminder.tone) }
var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) } var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) }
var selectedTrigger by remember { mutableStateOf(reminder.trigger) } 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) val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
@ -116,7 +120,8 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
isActive = isActive, isActive = isActive,
smoothSetting = smoothSetting, smoothSetting = smoothSetting,
trigger = selectedTrigger, trigger = selectedTrigger,
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15) isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15,
enabledRideProfiles = enabledRideProfiles.toSet())
} }
Column(modifier = Modifier Column(modifier = Modifier
@ -259,6 +264,41 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
) )
} }
Column(modifier = Modifier.fillMaxWidth()) {
if (enabledRideProfiles.isEmpty()) {
Text("Enabled for all profiles")
} else {
Text("Enabled for profiles:")
enabledRideProfiles.forEach { profileName ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(profileName)
FilledTonalButton(onClick = {
enabledRideProfiles = enabledRideProfiles.toMutableSet().apply { remove(profileName) }
}) {
Icon(Icons.Default.Delete, contentDescription = "Delete profile")
}
}
}
}
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp), onClick = {
rideProfileDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Change Profiles", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Limit to Profile")
}
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))
@ -310,6 +350,59 @@ 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 (rideProfileDialogVisible) {
var newProfileName by remember { mutableStateOf("") }
Dialog(onDismissRequest = { rideProfileDialogVisible = false }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
shape = RoundedCornerShape(10.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
OutlinedTextField(
value = newProfileName,
onValueChange = { newProfileName = it },
label = { Text("New profile name") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
FilledTonalButton(
onClick = {
if (newProfileName.isNotBlank()) {
enabledRideProfiles = enabledRideProfiles.toMutableSet().apply { add(newProfileName) }
newProfileName = ""
rideProfileDialogVisible = false
}
},
modifier = Modifier.fillMaxWidth().height(60.dp)
) {
Icon(Icons.Default.Add, contentDescription = "Add Profile")
Spacer(modifier = Modifier.width(5.dp))
Text("Add Profile")
}
FilledTonalButton(
onClick = { rideProfileDialogVisible = false },
modifier = Modifier.fillMaxWidth().height(60.dp)
) {
Icon(Icons.Default.Close, contentDescription = "Cancel Editing")
Spacer(modifier = Modifier.width(5.dp))
Text("Close")
}
}
}
}
}
if (triggerDialogVisible){ if (triggerDialogVisible){
Dialog(onDismissRequest = { triggerDialogVisible = false }) { Dialog(onDismissRequest = { triggerDialogVisible = false }) {
Card( Card(
@ -317,6 +410,7 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp), .padding(10.dp),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) { ) {
Column(modifier = Modifier Column(modifier = Modifier
.padding(5.dp) .padding(5.dp)
@ -361,6 +455,7 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp), .padding(10.dp),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) { ) {
Column(modifier = Modifier Column(modifier = Modifier
.padding(5.dp) .padding(5.dp)
@ -396,6 +491,7 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp), .padding(10.dp),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) { ) {
Column(modifier = Modifier Column(modifier = Modifier
.padding(5.dp) .padding(5.dp)
@ -430,3 +526,4 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
} }
} }
} }

View File

@ -20,13 +20,8 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -63,6 +58,7 @@ import androidx.navigation.navArgument
import de.timklge.karooreminder.KarooReminderExtension import de.timklge.karooreminder.KarooReminderExtension
import de.timklge.karooreminder.R import de.timklge.karooreminder.R
import de.timklge.karooreminder.dataStore import de.timklge.karooreminder.dataStore
import de.timklge.karooreminder.streamActiveRideProfile
import de.timklge.karooreminder.streamUserProfile import de.timklge.karooreminder.streamUserProfile
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile import io.hammerhead.karooext.models.UserProfile
@ -72,7 +68,6 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
val preferencesKey = stringPreferencesKey("reminders") val preferencesKey = stringPreferencesKey("reminders")
suspend fun saveReminders(context: Context, reminders: MutableList<Reminder>) { suspend fun saveReminders(context: Context, reminders: MutableList<Reminder>) {
@ -165,6 +160,7 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
var showWarnings by remember { mutableStateOf(false) } var showWarnings by remember { mutableStateOf(false) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null) val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
val currentRideProfile by karooSystem.streamActiveRideProfile().collectAsStateWithLifecycle(null)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
delay(1000L) delay(1000L)
@ -197,7 +193,7 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
Card(Modifier Card(Modifier
.fillMaxWidth() .fillMaxWidth()
.height(60.dp) .height(60.dp)
.alpha(if (reminder.isActive) 1f else 0.6f) .alpha(if (reminderIsActive(reminder, currentRideProfile?.profile)) 1f else 0.6f)
.clickable { onNavigateToReminder(reminder) } .clickable { onNavigateToReminder(reminder) }
.padding(5.dp), shape = RoundedCornerShape(corner = CornerSize(10.dp)) .padding(5.dp), shape = RoundedCornerShape(corner = CornerSize(10.dp))
) { ) {

View File

@ -7,6 +7,7 @@ import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.SmoothSetting import de.timklge.karooreminder.SmoothSetting
import io.hammerhead.karooext.models.PlayBeepPattern import io.hammerhead.karooext.models.PlayBeepPattern
import io.hammerhead.karooext.models.RideProfile
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
@ -141,6 +142,14 @@ class Reminder(val id: Int, var name: String,
val isActive: Boolean = true, val isAutoDismiss: Boolean = true, val isActive: Boolean = true, val isAutoDismiss: Boolean = true,
val tone: ReminderBeepPattern = ReminderBeepPattern.THREE_TONES_UP, val tone: ReminderBeepPattern = ReminderBeepPattern.THREE_TONES_UP,
var trigger: ReminderTrigger = ReminderTrigger.ELAPSED_TIME, var trigger: ReminderTrigger = ReminderTrigger.ELAPSED_TIME,
val autoDismissSeconds: Int = 15) val autoDismissSeconds: Int = 15,
val enabledRideProfiles: Set<String> = emptySet())
val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, text = "Take a sip!"))) 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 || enabledRideProfiles.isEmpty() || enabledRideProfiles.contains(currentProfileName))
}

View File

@ -22,7 +22,7 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "
[libraries] [libraries]
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
color = { module = "com.maxkeppeler.sheets-compose-dialogs:color", version.ref = "color" } 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" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }