Compare commits

..

No commits in common. "1.5" and "master" have entirely different histories.
1.5 ... master

11 changed files with 218 additions and 616 deletions

View File

@ -12,7 +12,6 @@ Compatible with Karoo 2 and Karoo 3 devices.
![Powerbar](powerbar0.png) ![Powerbar](powerbar0.png)
![Settings](powerbar1.png) ![Settings](powerbar1.png)
![Powerbar GIF](powerbar_min.gif) ![Powerbar GIF](powerbar_min.gif)
![Powerbar x4](powerbar2.png)
## Usage ## Usage
@ -26,9 +25,6 @@ to be displayed at the bottom or at the top of the screen:
- Average power over the last 10 seconds - Average power over the last 10 seconds
- Speed - Speed
- Cadence - Cadence
- Grade
- Route Progress (shows currently ridden distance)
- Remaining Route (shows remaining distance to the end of the route)
Subsequently, the bar(s) will be shown when riding. Bars are filled and colored according Subsequently, the bar(s) will be shown when riding. Bars are filled and colored according
to your current power output / heart rate zone as setup in your Karoo settings. Optionally, the actual data value can be displayed on top of the bar. to your current power output / heart rate zone as setup in your Karoo settings. Optionally, the actual data value can be displayed on top of the bar.

View File

@ -72,13 +72,11 @@ 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 adds colored power or heart rate progress bars to the edges of the screen, similar to the LEDs on Wahoo computers", "description" to "Open-source extension that adds colored power or heart rate progress bars to the edges of the screen, similar to the LEDs on Wahoo computers",
"releaseNotes" to "* Add option to split bars\n* Add grade data source", "releaseNotes" to "* Add route progress data source\n* Add workout target range indicator\n* Make bars transparent by default\n* Split size setting",
"screenshotUrls" to listOf( "screenshotUrls" to listOf(
"$baseUrl/powerbar_min.gif", "$baseUrl/powerbar_min.gif",
"$baseUrl/powerbar0.png", "$baseUrl/powerbar0.png",
"$baseUrl/powerbar2.png", "$baseUrl/powerbar2.png",
"$baseUrl/powerbar1.png",
"$baseUrl/powerbar3.png",
) )
) )

View File

@ -8,33 +8,15 @@ import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.graphics.Typeface import android.graphics.Typeface
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import de.timklge.karoopowerbar.screens.SelectedSource
class CustomView @JvmOverloads constructor( class CustomProgressBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : View(context, attrs) { ) : View(context, attrs) {
var progressBars: Map<HorizontalPowerbarLocation, CustomProgressBar>? = null
override fun onDrawForeground(canvas: Canvas) {
super.onDrawForeground(canvas)
// Draw all progress bars
progressBars?.values?.forEach { progressBar ->
Log.d(KarooPowerbarExtension.TAG, "Drawing progress bar for source: ${progressBar.source} - location: ${progressBar.location} - horizontalLocation: ${progressBar.horizontalLocation}")
progressBar.onDrawForeground(canvas)
}
}
}
class CustomProgressBar(private val view: CustomView,
val source: SelectedSource,
val location: PowerbarLocation,
val horizontalLocation: HorizontalPowerbarLocation) {
var progress: Double? = 0.5 var progress: Double? = 0.5
var location: PowerbarLocation = PowerbarLocation.BOTTOM
var label: String = "" var label: String = ""
var minTarget: Double? = null var minTarget: Double? = null
var maxTarget: Double? = null var maxTarget: Double? = null
@ -47,7 +29,7 @@ class CustomProgressBar(private val view: CustomView,
set(value) { set(value) {
field = value field = value
textPaint.textSize = value.fontSize textPaint.textSize = value.fontSize
view.invalidate() // Redraw to apply new font size invalidate() // Redraw to apply new font size
} }
var barSize = CustomProgressBarBarSize.MEDIUM var barSize = CustomProgressBarBarSize.MEDIUM
@ -63,7 +45,7 @@ class CustomProgressBar(private val view: CustomView,
CustomProgressBarBarSize.MEDIUM -> 8f CustomProgressBarBarSize.MEDIUM -> 8f
CustomProgressBarBarSize.LARGE -> 10f CustomProgressBarBarSize.LARGE -> 10f
} }
view.invalidate() // Redraw to apply new bar size invalidate() // Redraw to apply new bar size
} }
private val targetColor = 0xFF9933FF.toInt() private val targetColor = 0xFF9933FF.toInt()
@ -138,7 +120,9 @@ class CustomProgressBar(private val view: CustomView,
style = Paint.Style.STROKE style = Paint.Style.STROKE
} }
fun onDrawForeground(canvas: Canvas) { override fun onDrawForeground(canvas: Canvas) {
super.onDrawForeground(canvas)
// Determine if the current progress is within the target range // Determine if the current progress is within the target range
val isTargetMet = val isTargetMet =
progress != null && minTarget != null && maxTarget != null && progress!! >= minTarget!! && progress!! <= maxTarget!! progress != null && minTarget != null && maxTarget != null && progress!! >= minTarget!! && progress!! <= maxTarget!!
@ -148,65 +132,25 @@ class CustomProgressBar(private val view: CustomView,
blurPaint.color = progressColor blurPaint.color = progressColor
blurPaintHighlight.color = ColorUtils.blendARGB(progressColor, 0xFFFFFF, 0.5f) blurPaintHighlight.color = ColorUtils.blendARGB(progressColor, 0xFFFFFF, 0.5f)
val p = (progress ?: 0.0).coerceIn(0.0, 1.0)
val fullWidth = canvas.width.toFloat()
val halfWidth = fullWidth / 2f
val barLeft = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> 0f
HorizontalPowerbarLocation.RIGHT -> fullWidth - (halfWidth * p).toFloat()
HorizontalPowerbarLocation.FULL -> 0f
}
val barRight = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> (halfWidth * p).toFloat()
HorizontalPowerbarLocation.RIGHT -> fullWidth
HorizontalPowerbarLocation.FULL -> (fullWidth * p).toFloat()
}
val minTargetX = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> if (minTarget != null) (halfWidth * minTarget!!).toFloat() else 0f
HorizontalPowerbarLocation.RIGHT -> if (minTarget != null) halfWidth + (halfWidth * minTarget!!).toFloat() else 0f
HorizontalPowerbarLocation.FULL -> if (minTarget != null) (fullWidth * minTarget!!).toFloat() else 0f
}
val maxTargetX = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> if (maxTarget != null) (halfWidth * maxTarget!!).toFloat() else 0f
HorizontalPowerbarLocation.RIGHT -> if (maxTarget != null) halfWidth + (halfWidth * maxTarget!!).toFloat() else 0f
HorizontalPowerbarLocation.FULL -> if (maxTarget != null) (fullWidth * maxTarget!!).toFloat() else 0f
}
val targetX = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> if (target != null) (halfWidth * target!!).toFloat() else 0f
HorizontalPowerbarLocation.RIGHT -> if (target != null) halfWidth + (halfWidth * target!!).toFloat() else 0f
HorizontalPowerbarLocation.FULL -> if (target != null) (fullWidth * target!!).toFloat() else 0f
}
val backgroundLeft = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> 0f
HorizontalPowerbarLocation.RIGHT -> halfWidth
HorizontalPowerbarLocation.FULL -> 0f
}
val backgroundRight = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> halfWidth
HorizontalPowerbarLocation.RIGHT -> fullWidth
HorizontalPowerbarLocation.FULL -> fullWidth
}
when (location) { when (location) {
PowerbarLocation.TOP -> { PowerbarLocation.TOP -> {
val rect = RectF( val rect = RectF(
barLeft, 1f,
15f, 15f,
barRight, ((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(0.0, 1.0)).toFloat(),
15f + barSize.barHeight // barSize.barHeight will be 0f if NONE 15f + barSize.barHeight // barSize.barHeight will be 0f if NONE
) )
// Draw bar components only if barSize is not NONE // Draw bar components only if barSize is not NONE
if (barSize != CustomProgressBarBarSize.NONE) { if (barSize != CustomProgressBarBarSize.NONE) {
if (barBackground){ if (barBackground){
canvas.drawRect(backgroundLeft, 15f, backgroundRight, 15f + barSize.barHeight, backgroundPaint) canvas.drawRect(0f, 15f, canvas.width.toFloat(), 15f + barSize.barHeight, backgroundPaint)
} }
// Draw target zone fill behind the progress bar // Draw target zone fill behind the progress bar
if (minTarget != null && maxTarget != null) { if (minTarget != null && maxTarget != null) {
val minTargetX = (canvas.width * minTarget!!).toFloat()
val maxTargetX = (canvas.width * maxTarget!!).toFloat()
canvas.drawRoundRect( canvas.drawRoundRect(
minTargetX, minTargetX,
15f, 15f,
@ -228,6 +172,8 @@ class CustomProgressBar(private val view: CustomView,
if (progress != null) { if (progress != null) {
// Draw target zone stroke after progress bar, before label // Draw target zone stroke after progress bar, before label
if (minTarget != null && maxTarget != null) { if (minTarget != null && maxTarget != null) {
val minTargetX = (canvas.width * minTarget!!).toFloat()
val maxTargetX = (canvas.width * maxTarget!!).toFloat()
// Draw stroked rounded rectangle for the target zone // Draw stroked rounded rectangle for the target zone
canvas.drawRoundRect( canvas.drawRoundRect(
minTargetX, minTargetX,
@ -242,6 +188,7 @@ class CustomProgressBar(private val view: CustomView,
// Draw vertical target indicator line if target is present // Draw vertical target indicator line if target is present
if (target != null) { if (target != null) {
val targetX = (canvas.width * target!!).toFloat()
targetIndicatorPaint.color = if (isTargetMet) Color.GREEN else Color.RED targetIndicatorPaint.color = if (isTargetMet) Color.GREEN else Color.RED
canvas.drawLine(targetX, 15f, targetX, 15f + barSize.barHeight, targetIndicatorPaint) canvas.drawLine(targetX, 15f, targetX, 15f + barSize.barHeight, targetIndicatorPaint)
} }
@ -256,7 +203,7 @@ class CustomProgressBar(private val view: CustomView,
val textBounds = textPaint.measureText(label) val textBounds = textPaint.measureText(label)
val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f
val x = (if (horizontalLocation != HorizontalPowerbarLocation.RIGHT) rect.right - xOffset else rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f)
val r = x + xOffset * 2 val r = x + xOffset * 2
val fm = textPaint.fontMetrics val fm = textPaint.fontMetrics
@ -279,9 +226,9 @@ class CustomProgressBar(private val view: CustomView,
PowerbarLocation.BOTTOM -> { PowerbarLocation.BOTTOM -> {
val rect = RectF( val rect = RectF(
barLeft, 1f,
canvas.height.toFloat() - 1f - barSize.barHeight, // barSize.barHeight will be 0f if NONE canvas.height.toFloat() - 1f - barSize.barHeight, // barSize.barHeight will be 0f if NONE
barRight, ((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(0.0, 1.0)).toFloat(),
canvas.height.toFloat() canvas.height.toFloat()
) )
@ -289,11 +236,13 @@ class CustomProgressBar(private val view: CustomView,
if (barSize != CustomProgressBarBarSize.NONE) { if (barSize != CustomProgressBarBarSize.NONE) {
if (barBackground){ if (barBackground){
// Use barSize.barHeight for background top calculation // Use barSize.barHeight for background top calculation
canvas.drawRect(backgroundLeft, canvas.height.toFloat() - barSize.barHeight, backgroundRight, canvas.height.toFloat(), backgroundPaint) canvas.drawRect(0f, canvas.height.toFloat() - barSize.barHeight, canvas.width.toFloat(), canvas.height.toFloat(), backgroundPaint)
} }
// Draw target zone fill behind the progress bar // Draw target zone fill behind the progress bar
if (minTarget != null && maxTarget != null) { if (minTarget != null && maxTarget != null) {
val minTargetX = (canvas.width * minTarget!!).toFloat()
val maxTargetX = (canvas.width * maxTarget!!).toFloat()
canvas.drawRoundRect( canvas.drawRoundRect(
minTargetX, minTargetX,
canvas.height.toFloat() - barSize.barHeight, canvas.height.toFloat() - barSize.barHeight,
@ -316,6 +265,8 @@ class CustomProgressBar(private val view: CustomView,
if (progress != null) { if (progress != null) {
// Draw target zone stroke after progress bar, before label // Draw target zone stroke after progress bar, before label
if (minTarget != null && maxTarget != null) { if (minTarget != null && maxTarget != null) {
val minTargetX = (canvas.width * minTarget!!).toFloat()
val maxTargetX = (canvas.width * maxTarget!!).toFloat()
// Draw stroked rounded rectangle for the target zone // Draw stroked rounded rectangle for the target zone
canvas.drawRoundRect( canvas.drawRoundRect(
minTargetX, minTargetX,
@ -330,6 +281,7 @@ class CustomProgressBar(private val view: CustomView,
// Draw vertical target indicator line if target is present // Draw vertical target indicator line if target is present
if (target != null) { if (target != null) {
val targetX = (canvas.width * target!!).toFloat()
targetIndicatorPaint.color = if (isTargetMet) Color.GREEN else Color.RED targetIndicatorPaint.color = if (isTargetMet) Color.GREEN else Color.RED
canvas.drawLine(targetX, canvas.height.toFloat() - barSize.barHeight, targetX, canvas.height.toFloat(), targetIndicatorPaint) canvas.drawLine(targetX, canvas.height.toFloat() - barSize.barHeight, targetX, canvas.height.toFloat(), targetIndicatorPaint)
} }
@ -344,7 +296,7 @@ class CustomProgressBar(private val view: CustomView,
val textBounds = textPaint.measureText(label) val textBounds = textPaint.measureText(label)
val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f
val x = (if (horizontalLocation != HorizontalPowerbarLocation.RIGHT) rect.right - xOffset else rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f)
val r = x + xOffset * 2 val r = x + xOffset * 2
// textDrawBaselineY calculation uses rect.top and barSize.barHeight. // textDrawBaselineY calculation uses rect.top and barSize.barHeight.
@ -363,9 +315,4 @@ class CustomProgressBar(private val view: CustomView,
} }
} }
} }
fun invalidate() {
// Invalidate the view to trigger a redraw
view.invalidate()
}
} }

View File

@ -52,18 +52,17 @@ class ForegroundService : Service() {
windows.forEach { it.close() } windows.forEach { it.close() }
windows.clear() windows.clear()
if (showBars){ if (settings.source != SelectedSource.NONE && showBars) {
if (settings.bottomBarSource != SelectedSource.NONE || settings.bottomBarLeftSource != SelectedSource.NONE || settings.bottomBarRightSource != SelectedSource.NONE) { Window(this@ForegroundService, PowerbarLocation.BOTTOM, settings.showLabelOnBars, settings.barBackground, settings.barBarSize, settings.barFontSize).apply {
Window(this@ForegroundService, PowerbarLocation.BOTTOM, settings.showLabelOnBars, settings.barBackground, settings.barBarSize, settings.barFontSize, selectedSource = settings.source
settings.splitBottomBar, settings.bottomBarSource, settings.bottomBarLeftSource, settings.bottomBarRightSource).apply {
windows.add(this) windows.add(this)
open() open()
} }
} }
if (settings.topBarSource != SelectedSource.NONE || settings.topBarLeftSource != SelectedSource.NONE || settings.topBarRightSource != SelectedSource.NONE) { if (settings.topBarSource != SelectedSource.NONE && showBars){
Window(this@ForegroundService, PowerbarLocation.TOP, settings.showLabelOnBars, settings.barBackground, settings.barBarSize, settings.barFontSize, Window(this@ForegroundService, PowerbarLocation.TOP, settings.showLabelOnBars, settings.barBackground, settings.barBarSize, settings.barFontSize).apply {
settings.splitTopBar, settings.topBarSource, settings.topBarLeftSource, settings.topBarRightSource).apply { selectedSource = settings.topBarSource
open() open()
windows.add(this) windows.add(this)
} }
@ -71,7 +70,6 @@ class ForegroundService : Service() {
} }
} }
} }
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)

View File

@ -7,7 +7,6 @@ import de.timklge.karoopowerbar.screens.SelectedSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.SerialName
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
@ -16,16 +15,8 @@ val settingsKey = stringPreferencesKey("settings")
@Serializable @Serializable
data class PowerbarSettings( data class PowerbarSettings(
@SerialName("source") val bottomBarSource: SelectedSource = SelectedSource.POWER, val source: SelectedSource = SelectedSource.POWER,
val topBarSource: SelectedSource = SelectedSource.NONE, val topBarSource: SelectedSource = SelectedSource.NONE,
val splitTopBar: Boolean = false,
val splitBottomBar: Boolean = false,
val topBarLeftSource: SelectedSource = SelectedSource.NONE,
val topBarRightSource: SelectedSource = SelectedSource.NONE,
val bottomBarLeftSource: SelectedSource = SelectedSource.POWER,
val bottomBarRightSource: SelectedSource = SelectedSource.NONE,
val onlyShowWhileRiding: Boolean = true, val onlyShowWhileRiding: Boolean = true,
val showLabelOnBars: Boolean = true, val showLabelOnBars: Boolean = true,
val useZoneColors: Boolean = true, val useZoneColors: Boolean = true,
@ -38,11 +29,7 @@ data class PowerbarSettings(
val minSpeed: Float = defaultMinSpeedMs, val maxSpeed: Float = defaultMaxSpeedMs, // 50 km/h in m/s val minSpeed: Float = defaultMinSpeedMs, val maxSpeed: Float = defaultMaxSpeedMs, // 50 km/h in m/s
val minPower: Int? = null, val maxPower: Int? = null, val minPower: Int? = null, val maxPower: Int? = null,
val minHr: Int? = null, val maxHr: Int? = null, val minHr: Int? = null, val maxHr: Int? = null,
val minGradient: Int? = defaultMinGradient, val maxGradient: Int? = defaultMaxGradient, val useCustomHrRange: Boolean = false, val useCustomPowerRange: Boolean = false
val useCustomGradientRange: Boolean = false,
val useCustomHrRange: Boolean = false,
val useCustomPowerRange: Boolean = false
){ ){
companion object { companion object {
val defaultSettings = Json.encodeToString(PowerbarSettings()) val defaultSettings = Json.encodeToString(PowerbarSettings())
@ -50,8 +37,6 @@ data class PowerbarSettings(
const val defaultMaxSpeedMs = 13.89f const val defaultMaxSpeedMs = 13.89f
const val defaultMinCadence = 50 const val defaultMinCadence = 50
const val defaultMaxCadence = 120 const val defaultMaxCadence = 120
const val defaultMinGradient = 0
const val defaultMaxGradient = 15
} }
} }

View File

@ -34,10 +34,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
fun remap(value: Double?, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double? { fun remap(value: Double?, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double? {
@ -50,10 +50,6 @@ enum class PowerbarLocation {
TOP, BOTTOM TOP, BOTTOM
} }
enum class HorizontalPowerbarLocation {
FULL, LEFT, RIGHT
}
class Window( class Window(
private val context: Context, private val context: Context,
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM, val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM,
@ -61,10 +57,6 @@ class Window(
val barBackground: Boolean, val barBackground: Boolean,
val powerbarBarSize: CustomProgressBarBarSize, val powerbarBarSize: CustomProgressBarBarSize,
val powerbarFontSize: CustomProgressBarFontSize, val powerbarFontSize: CustomProgressBarFontSize,
val splitBars: Boolean,
val selectedSource: SelectedSource = SelectedSource.NONE,
val selectedLeftSource: SelectedSource = SelectedSource.NONE,
val selectedRightSource: SelectedSource = SelectedSource.NONE
) { ) {
companion object { companion object {
val FIELD_TARGET_VALUE_ID = "FIELD_WORKOUT_TARGET_VALUE_ID"; val FIELD_TARGET_VALUE_ID = "FIELD_WORKOUT_TARGET_VALUE_ID";
@ -77,8 +69,9 @@ class Window(
private val windowManager: WindowManager private val windowManager: WindowManager
private val layoutInflater: LayoutInflater private val layoutInflater: LayoutInflater
private val powerbars: MutableMap<HorizontalPowerbarLocation, CustomProgressBar> = mutableMapOf() private val powerbar: CustomProgressBar
private val view: CustomView
var selectedSource: SelectedSource = SelectedSource.POWER
init { init {
layoutParams = WindowManager.LayoutParams( layoutParams = WindowManager.LayoutParams(
@ -91,8 +84,8 @@ class Window(
layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
rootView = layoutInflater.inflate(R.layout.popup_window, null) rootView = layoutInflater.inflate(R.layout.popup_window, null)
view = rootView.findViewById(R.id.customView) powerbar = rootView.findViewById(R.id.progressBar)
view.progressBars = powerbars powerbar.progress = null
windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
@ -123,10 +116,11 @@ class Window(
private val karooSystem: KarooSystemService = KarooSystemService(context) private val karooSystem: KarooSystemService = KarooSystemService(context)
private var serviceJobs: MutableSet<Job> = mutableSetOf() private var serviceJob: Job? = null
@SuppressLint("UnspecifiedRegisterReceiverFlag") @SuppressLint("UnspecifiedRegisterReceiverFlag")
suspend fun open() { suspend fun open() {
serviceJob = CoroutineScope(Dispatchers.Default).launch {
val filter = IntentFilter("de.timklge.HIDE_POWERBAR") val filter = IntentFilter("de.timklge.HIDE_POWERBAR")
if (Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
context.registerReceiver(hideReceiver, filter, Context.RECEIVER_EXPORTED) context.registerReceiver(hideReceiver, filter, Context.RECEIVER_EXPORTED)
@ -138,53 +132,29 @@ class Window(
Log.i(TAG, "Karoo system service connected: $connected") Log.i(TAG, "Karoo system service connected: $connected")
} }
powerbars.clear()
if (!splitBars) {
if (selectedSource != SelectedSource.NONE){
powerbars[HorizontalPowerbarLocation.FULL] = CustomProgressBar(view, selectedSource, powerbarLocation, HorizontalPowerbarLocation.FULL)
}
} else {
if (selectedLeftSource != SelectedSource.NONE) {
powerbars[HorizontalPowerbarLocation.LEFT] = CustomProgressBar(view, selectedLeftSource, powerbarLocation, HorizontalPowerbarLocation.LEFT)
}
if (selectedRightSource != SelectedSource.NONE) {
powerbars[HorizontalPowerbarLocation.RIGHT] = CustomProgressBar(view, selectedRightSource, powerbarLocation, HorizontalPowerbarLocation.RIGHT)
}
}
powerbars.values.forEach { powerbar ->
powerbar.progressColor = context.resources.getColor(R.color.zone7) powerbar.progressColor = context.resources.getColor(R.color.zone7)
powerbar.progress = null powerbar.progress = null
powerbar.location = powerbarLocation
powerbar.showLabel = showLabel powerbar.showLabel = showLabel
powerbar.barBackground = barBackground powerbar.barBackground = barBackground
powerbar.fontSize = powerbarFontSize powerbar.fontSize = powerbarFontSize
powerbar.barSize = powerbarBarSize powerbar.barSize = powerbarBarSize
powerbar.invalidate() powerbar.invalidate()
}
Log.i(TAG, "Streaming $selectedSource") Log.i(TAG, "Streaming $selectedSource")
val selectedSources = powerbars.values.map { it.source }.toSet()
selectedSources.forEach { selectedSource ->
serviceJobs.add( CoroutineScope(Dispatchers.IO).launch {
Log.i(TAG, "Starting stream for $selectedSource")
when (selectedSource){ when (selectedSource){
SelectedSource.POWER -> streamPower(SelectedSource.POWER, PowerStreamSmoothing.RAW) SelectedSource.POWER -> streamPower(PowerStreamSmoothing.RAW)
SelectedSource.POWER_3S -> streamPower(SelectedSource.POWER_3S, PowerStreamSmoothing.SMOOTHED_3S) SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S)
SelectedSource.POWER_10S -> streamPower(SelectedSource.POWER_10S, PowerStreamSmoothing.SMOOTHED_10S) SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S)
SelectedSource.HEART_RATE -> streamHeartrate() SelectedSource.HEART_RATE -> streamHeartrate()
SelectedSource.SPEED -> streamSpeed(SelectedSource.SPEED, false) SelectedSource.SPEED -> streamSpeed(false)
SelectedSource.SPEED_3S -> streamSpeed(SelectedSource.SPEED_3S, true) SelectedSource.SPEED_3S -> streamSpeed(true)
SelectedSource.CADENCE -> streamCadence(SelectedSource.CADENCE, false) SelectedSource.CADENCE -> streamCadence(false)
SelectedSource.CADENCE_3S -> streamCadence(SelectedSource.CADENCE_3S, true) SelectedSource.CADENCE_3S -> streamCadence(true)
SelectedSource.ROUTE_PROGRESS -> streamRouteProgress(SelectedSource.ROUTE_PROGRESS, ::getRouteProgress) SelectedSource.ROUTE_PROGRESS -> streamRouteProgress()
SelectedSource.REMAINING_ROUTE -> streamRouteProgress(SelectedSource.REMAINING_ROUTE, ::getRemainingRouteProgress) else -> {}
SelectedSource.GRADE -> streamGrade()
SelectedSource.NONE -> {}
} }
})
} }
try { try {
@ -198,53 +168,19 @@ class Window(
} }
} }
data class BarProgress( private suspend fun streamRouteProgress() {
val progress: Double?,
val label: String,
)
private fun getRouteProgress(userProfile: UserProfile, riddenDistance: Double?, routeEndAt: Double?, distanceToDestination: Double?): BarProgress {
val routeProgress = if (routeEndAt != null && riddenDistance != null) remap(riddenDistance, 0.0, routeEndAt, 0.0, 1.0) else null
val routeProgressInUserUnit = when (userProfile.preferredUnit.distance) {
UserProfile.PreferredUnit.UnitType.IMPERIAL -> riddenDistance?.times(0.000621371)?.roundToInt() // Miles
else -> riddenDistance?.times(0.001)?.roundToInt() // Kilometers
}
return BarProgress(routeProgress, "$routeProgressInUserUnit")
}
private fun getRemainingRouteProgress(userProfile: UserProfile, riddenDistance: Double?, routeEndAt: Double?, distanceToDestination: Double?): BarProgress {
val routeProgress = if (routeEndAt != null && riddenDistance != null) remap(riddenDistance, 0.0, routeEndAt, 0.0, 1.0) else null
val distanceToDestinationInUserUnit = when (userProfile.preferredUnit.distance) {
UserProfile.PreferredUnit.UnitType.IMPERIAL -> distanceToDestination?.times(0.000621371)?.roundToInt() // Miles
else -> distanceToDestination?.times(0.001)?.roundToInt() // Kilometers
}
return BarProgress(routeProgress, "$distanceToDestinationInUserUnit")
}
private suspend fun streamRouteProgress(
source: SelectedSource,
routeProgressProvider: (UserProfile, Double?, Double?, Double?) -> BarProgress
) {
data class StreamData( data class StreamData(
val userProfile: UserProfile, val userProfile: UserProfile,
val distanceToDestination: Double?, val distanceToDestination: Double?,
val navigationState: OnNavigationState, val navigationState: OnNavigationState
val riddenDistance: Double?
) )
var lastKnownRoutePolyline: String? = null var lastKnownRoutePolyline: String? = null
var lastKnownRouteLength: Double? = null var lastKnownRouteLength: Double? = null
combine(karooSystem.streamUserProfile(), karooSystem.streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION), karooSystem.streamNavigationState(), karooSystem.streamDataFlow(DataType.Type.DISTANCE)) { userProfile, distanceToDestination, navigationState, riddenDistance -> combine(karooSystem.streamUserProfile(), karooSystem.streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION), karooSystem.streamNavigationState()) { userProfile, distanceToDestination, navigationState ->
StreamData( StreamData(userProfile, (distanceToDestination as? StreamState.Streaming)?.dataPoint?.values[DataType.Field.DISTANCE_TO_DESTINATION], navigationState)
userProfile, }.distinctUntilChanged().throttle(5_000).collect { (userProfile, distanceToDestination, navigationState) ->
(distanceToDestination as? StreamState.Streaming)?.dataPoint?.values?.get(DataType.Field.DISTANCE_TO_DESTINATION),
navigationState,
(riddenDistance as? StreamState.Streaming)?.dataPoint?.values?.get(DataType.Field.DISTANCE)
)
}.distinctUntilChanged().throttle(5_000).collect { (userProfile, distanceToDestination, navigationState, riddenDistance) ->
val state = navigationState.state val state = navigationState.state
val routePolyline = when (state) { val routePolyline = when (state) {
is OnNavigationState.NavigationState.NavigatingRoute -> state.routePolyline is OnNavigationState.NavigationState.NavigatingRoute -> state.routePolyline
@ -266,21 +202,22 @@ class Window(
} }
} }
val routeEndAt = lastKnownRouteLength?.plus((distanceToDestination ?: 0.0)) val routeLength = lastKnownRouteLength
val barProgress = routeProgressProvider(userProfile, riddenDistance, routeEndAt, distanceToDestination) val routeProgressMeters = routeLength?.let { routeLength - (distanceToDestination ?: 0.0) }?.coerceAtLeast(0.0)
val routeProgress = if (routeLength != null && routeProgressMeters != null) remap(routeProgressMeters, 0.0, routeLength, 0.0, 1.0) else null
val routeProgressInUserUnit = when (userProfile.preferredUnit.distance) {
UserProfile.PreferredUnit.UnitType.IMPERIAL -> routeProgressMeters?.times(0.000621371)?.roundToInt() // Miles
else -> routeProgressMeters?.times(0.001)?.roundToInt() // Kilometers
}
val powerbarsWithRouteProgressSource = powerbars.values.filter { it.source == source }
powerbarsWithRouteProgressSource.forEach { powerbar ->
powerbar.progressColor = context.getColor(R.color.zone0) powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = barProgress.progress powerbar.progress = routeProgress
powerbar.label = barProgress.label powerbar.label = "$routeProgressInUserUnit"
powerbar.invalidate() powerbar.invalidate()
} }
} }
}
private suspend fun streamSpeed(source: SelectedSource, smoothed: Boolean) { private suspend fun streamSpeed(smoothed: Boolean) {
val speedFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_SPEED else DataType.Type.SPEED) val speedFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_SPEED else DataType.Type.SPEED)
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.distinctUntilChanged() .distinctUntilChanged()
@ -298,9 +235,7 @@ class Window(
else -> valueMetersPerSecond?.times(3.6) else -> valueMetersPerSecond?.times(3.6)
}?.roundToInt() }?.roundToInt()
val powerbarsWithSpeedSource = powerbars.values.filter { it.source == source } if (value != null && valueMetersPerSecond != null) {
powerbarsWithSpeedSource.forEach { powerbar ->
if (value != null) {
val minSpeed = streamData.settings?.minSpeed ?: PowerbarSettings.defaultMinSpeedMs val minSpeed = streamData.settings?.minSpeed ?: PowerbarSettings.defaultMinSpeedMs
val maxSpeed = streamData.settings?.maxSpeed ?: PowerbarSettings.defaultMaxSpeedMs val maxSpeed = streamData.settings?.maxSpeed ?: PowerbarSettings.defaultMaxSpeedMs
val progress = remap(valueMetersPerSecond, minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) ?: 0.0 val progress = remap(valueMetersPerSecond, minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) ?: 0.0
@ -326,66 +261,8 @@ class Window(
powerbar.invalidate() powerbar.invalidate()
} }
} }
}
private suspend fun streamGrade() { private suspend fun streamCadence(smoothed: Boolean) {
@ColorRes
fun getInclineIndicatorColor(percent: Float): Int? {
return when(percent) {
in -Float.MAX_VALUE..<-7.5f -> R.color.eleDarkBlue // Dark blue
in -7.5f..<-4.6f -> R.color.eleLightBlue // Light blue
in -4.6f..<-2f -> R.color.eleWhite // White
in -2f..<2f -> R.color.eleGray // Gray
in 2f..<4.6f -> R.color.eleDarkGreen // Dark green
in 4.6f..<7.5f -> R.color.eleLightGreen // Light green
in 7.5f..<12.5f -> R.color.eleYellow // Yellow
in 12.5f..<15.5f -> R.color.eleLightOrange // Light Orange
in 15.5f..<19.5f -> R.color.eleDarkOrange // Dark Orange
in 19.5f..<23.5f -> R.color.eleRed // Red
in 23.5f..Float.MAX_VALUE -> R.color.elePurple // Purple
else -> null
}
}
val gradeFlow = karooSystem.streamDataFlow(DataType.Type.ELEVATION_GRADE)
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.distinctUntilChanged()
data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null)
val settingsFlow = context.streamSettings()
combine(karooSystem.streamUserProfile(), gradeFlow, settingsFlow) { userProfile, grade, settings ->
StreamData(userProfile, grade, settings)
}.distinctUntilChanged().throttle(1_000).collect { streamData ->
val value = streamData.value
val powerbarsWithGradeSource = powerbars.values.filter { it.source == SelectedSource.GRADE }
powerbarsWithGradeSource.forEach { powerbar ->
if (value != null) {
val minGradient = streamData.settings?.minGradient ?: PowerbarSettings.defaultMinGradient
val maxGradient = streamData.settings?.maxGradient ?: PowerbarSettings.defaultMaxGradient
val colorRes = getInclineIndicatorColor(value.toFloat()) ?: R.color.zone0
powerbar.progressColor = context.getColor(colorRes)
powerbar.progress = remap(value.toDouble(), minGradient.toDouble(), maxGradient.toDouble(), 0.0, 1.0)
powerbar.label = "${String.format(Locale.getDefault(), "%.1f", value)}%"
Log.d(TAG, "Grade: $value")
} else {
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Grade: Unavailable")
}
powerbar.invalidate()
}
}
}
private suspend fun streamCadence(source: SelectedSource, smoothed: Boolean) {
val cadenceFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE else DataType.Type.CADENCE) val cadenceFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE else DataType.Type.CADENCE)
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.distinctUntilChanged() .distinctUntilChanged()
@ -401,17 +278,15 @@ class Window(
StreamData(userProfile, speed, settings, cadenceTarget) StreamData(userProfile, speed, settings, cadenceTarget)
}.distinctUntilChanged().throttle(1_000).collect { streamData -> }.distinctUntilChanged().throttle(1_000).collect { streamData ->
val value = streamData.value?.roundToInt() val value = streamData.value?.roundToInt()
val powerbarsWithCadenceSource = powerbars.values.filter { it.source == source }
powerbarsWithCadenceSource.forEach { powerbar ->
if (value != null) { if (value != null) {
val minCadence = streamData.settings?.minCadence ?: PowerbarSettings.defaultMinCadence val minCadence = streamData.settings?.minCadence ?: PowerbarSettings.defaultMinCadence
val maxCadence = streamData.settings?.maxCadence ?: PowerbarSettings.defaultMaxCadence val maxCadence = streamData.settings?.maxCadence ?: PowerbarSettings.defaultMaxCadence
val progress = remap(value.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) ?: 0.0 val progress = remap(value.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) ?: 0.0
powerbar.minTarget = remap(streamData.cadenceTarget?.values?.get(FIELD_TARGET_MIN_ID)?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) powerbar.minTarget = remap(streamData.cadenceTarget?.values[FIELD_TARGET_MIN_ID]?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0)
powerbar.maxTarget = remap(streamData.cadenceTarget?.values?.get(FIELD_TARGET_MAX_ID)?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) powerbar.maxTarget = remap(streamData.cadenceTarget?.values[FIELD_TARGET_MAX_ID]?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0)
powerbar.target = remap(streamData.cadenceTarget?.values?.get(FIELD_TARGET_VALUE_ID)?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) powerbar.target = remap(streamData.cadenceTarget?.values[FIELD_TARGET_VALUE_ID]?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0)
@ColorRes val zoneColorRes = Zone.entries[(progress * Zone.entries.size).roundToInt().coerceIn(0..<Zone.entries.size)].colorResource @ColorRes val zoneColorRes = Zone.entries[(progress * Zone.entries.size).roundToInt().coerceIn(0..<Zone.entries.size)].colorResource
@ -434,7 +309,6 @@ class Window(
powerbar.invalidate() powerbar.invalidate()
} }
} }
}
private suspend fun streamHeartrate() { private suspend fun streamHeartrate() {
val hrFlow = karooSystem.streamDataFlow(DataType.Type.HEART_RATE) val hrFlow = karooSystem.streamDataFlow(DataType.Type.HEART_RATE)
@ -452,9 +326,7 @@ class Window(
StreamData(userProfile, hr, settings, hrTarget) StreamData(userProfile, hr, settings, hrTarget)
}.distinctUntilChanged().throttle(1_000).collect { streamData -> }.distinctUntilChanged().throttle(1_000).collect { streamData ->
val value = streamData.value?.roundToInt() val value = streamData.value?.roundToInt()
val powerbarsWithHrSource = powerbars.values.filter { it.source == SelectedSource.HEART_RATE }
powerbarsWithHrSource.forEach { powerbar ->
if (value != null) { if (value != null) {
val customMinHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.minHr else null val customMinHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.minHr else null
val customMaxHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.maxHr else null val customMaxHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.maxHr else null
@ -462,9 +334,9 @@ class Window(
val maxHr = customMaxHr ?: streamData.userProfile.maxHr val maxHr = customMaxHr ?: streamData.userProfile.maxHr
val progress = remap(value.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) val progress = remap(value.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0)
powerbar.minTarget = remap(streamData.heartrateTarget?.values?.get(FIELD_TARGET_MIN_ID), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) powerbar.minTarget = remap(streamData.heartrateTarget?.values[FIELD_TARGET_MIN_ID]?.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0)
powerbar.maxTarget = remap(streamData.heartrateTarget?.values?.get(FIELD_TARGET_MAX_ID), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) powerbar.maxTarget = remap(streamData.heartrateTarget?.values[FIELD_TARGET_MAX_ID]?.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0)
powerbar.target = remap(streamData.heartrateTarget?.values?.get(FIELD_TARGET_VALUE_ID), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) powerbar.target = remap(streamData.heartrateTarget?.values[FIELD_TARGET_VALUE_ID]?.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0)
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
context.getColor(getZone(streamData.userProfile.heartRateZones, value)?.colorResource ?: R.color.zone7) context.getColor(getZone(streamData.userProfile.heartRateZones, value)?.colorResource ?: R.color.zone7)
@ -485,7 +357,6 @@ class Window(
powerbar.invalidate() powerbar.invalidate()
} }
} }
}
enum class PowerStreamSmoothing(val dataTypeId: String){ enum class PowerStreamSmoothing(val dataTypeId: String){
RAW(DataType.Type.POWER), RAW(DataType.Type.POWER),
@ -493,7 +364,7 @@ class Window(
SMOOTHED_10S(DataType.Type.SMOOTHED_10S_AVERAGE_POWER), SMOOTHED_10S(DataType.Type.SMOOTHED_10S_AVERAGE_POWER),
} }
private suspend fun streamPower(source: SelectedSource, smoothed: PowerStreamSmoothing) { private suspend fun streamPower(smoothed: PowerStreamSmoothing) {
val powerFlow = karooSystem.streamDataFlow(smoothed.dataTypeId) val powerFlow = karooSystem.streamDataFlow(smoothed.dataTypeId)
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.distinctUntilChanged() .distinctUntilChanged()
@ -510,9 +381,7 @@ class Window(
StreamData(userProfile, hr, settings, powerTarget) StreamData(userProfile, hr, settings, powerTarget)
}.distinctUntilChanged().throttle(1_000).collect { streamData -> }.distinctUntilChanged().throttle(1_000).collect { streamData ->
val value = streamData.value?.roundToInt() val value = streamData.value?.roundToInt()
val powerbarsWithPowerSource = powerbars.values.filter { it.source == source }
powerbarsWithPowerSource.forEach { powerbar ->
if (value != null) { if (value != null) {
val customMinPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.minPower else null val customMinPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.minPower else null
val customMaxPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.maxPower else null val customMaxPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.maxPower else null
@ -520,9 +389,9 @@ class Window(
val maxPower = customMaxPower ?: (streamData.userProfile.powerZones.last().min + 30) val maxPower = customMaxPower ?: (streamData.userProfile.powerZones.last().min + 30)
val progress = remap(value.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) val progress = remap(value.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0)
powerbar.minTarget = remap(streamData.powerTarget?.values?.get(FIELD_TARGET_MIN_ID), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) powerbar.minTarget = remap(streamData.powerTarget?.values[FIELD_TARGET_MIN_ID]?.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0)
powerbar.maxTarget = remap(streamData.powerTarget?.values?.get(FIELD_TARGET_MAX_ID), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) powerbar.maxTarget = remap(streamData.powerTarget?.values[FIELD_TARGET_MAX_ID]?.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0)
powerbar.target = remap(streamData.powerTarget?.values?.get(FIELD_TARGET_VALUE_ID), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) powerbar.target = remap(streamData.powerTarget?.values[FIELD_TARGET_VALUE_ID]?.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0)
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
context.getColor(getZone(streamData.userProfile.powerZones, value)?.colorResource ?: R.color.zone7) context.getColor(getZone(streamData.userProfile.powerZones, value)?.colorResource ?: R.color.zone7)
@ -543,7 +412,6 @@ class Window(
powerbar.invalidate() powerbar.invalidate()
} }
} }
}
private var currentHideJob: Job? = null private var currentHideJob: Job? = null
@ -554,9 +422,7 @@ class Window(
currentHideJob?.cancel() currentHideJob?.cancel()
currentHideJob = null currentHideJob = null
} }
serviceJobs.forEach { job -> serviceJob?.cancel()
job.cancel()
}
(context.getSystemService(WINDOW_SERVICE) as WindowManager).removeView(rootView) (context.getSystemService(WINDOW_SERVICE) as WindowManager).removeView(rootView)
rootView.invalidate() rootView.invalidate()
(rootView.parent as? ViewGroup)?.removeAllViews() (rootView.parent as? ViewGroup)?.removeAllViews()

View File

@ -59,6 +59,7 @@ import androidx.datastore.preferences.core.edit
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import de.timklge.karoopowerbar.CustomProgressBarBarSize import de.timklge.karoopowerbar.CustomProgressBarBarSize
import de.timklge.karoopowerbar.CustomProgressBarFontSize import de.timklge.karoopowerbar.CustomProgressBarFontSize
import de.timklge.karoopowerbar.CustomProgressBarSize
import de.timklge.karoopowerbar.KarooPowerbarExtension import de.timklge.karoopowerbar.KarooPowerbarExtension
import de.timklge.karoopowerbar.PowerbarSettings import de.timklge.karoopowerbar.PowerbarSettings
import de.timklge.karoopowerbar.R import de.timklge.karoopowerbar.R
@ -84,12 +85,10 @@ enum class SelectedSource(val id: String, val label: String) {
POWER_3S("power_3s", "Power (3 sec avg)"), POWER_3S("power_3s", "Power (3 sec avg)"),
POWER_10S("power_10s", "Power (10 sec avg)"), POWER_10S("power_10s", "Power (10 sec avg)"),
SPEED("speed", "Speed"), SPEED("speed", "Speed"),
SPEED_3S("speed_3s", "Speed (3 sec avg)"), SPEED_3S("speed_3s", "Speed (3 sec avg"),
CADENCE("cadence", "Cadence"), CADENCE("cadence", "Cadence"),
CADENCE_3S("cadence_3s", "Cadence (3 sec avg)"), CADENCE_3S("cadence_3s", "Cadence (3 sec avg)"),
GRADE("grade", "Grade"), ROUTE_PROGRESS("route_progress", "Route Progress");
ROUTE_PROGRESS("route_progress", "Route Progress"),
REMAINING_ROUTE("route_progress_remaining", "Route Remaining");
fun isPower() = this == POWER || this == POWER_3S || this == POWER_10S fun isPower() = this == POWER || this == POWER_3S || this == POWER_10S
} }
@ -139,22 +138,9 @@ fun MainScreen(onFinish: () -> Unit) {
var bottomSelectedSource by remember { mutableStateOf(SelectedSource.POWER) } var bottomSelectedSource by remember { mutableStateOf(SelectedSource.POWER) }
var topSelectedSource by remember { mutableStateOf(SelectedSource.NONE) } var topSelectedSource by remember { mutableStateOf(SelectedSource.NONE) }
var splitTopBar by remember { mutableStateOf(false) }
var splitBottomBar by remember { mutableStateOf(false) }
var topSelectedSourceLeft by remember { mutableStateOf(SelectedSource.NONE) }
var topSelectedSourceRight by remember { mutableStateOf(SelectedSource.NONE) }
var bottomSelectedSourceLeft by remember { mutableStateOf(SelectedSource.NONE) }
var bottomSelectedSourceRight by remember { mutableStateOf(SelectedSource.NONE) }
var bottomBarDialogVisible by remember { mutableStateOf(false) } var bottomBarDialogVisible by remember { mutableStateOf(false) }
var topBarDialogVisible by remember { mutableStateOf(false) } var topBarDialogVisible by remember { mutableStateOf(false) }
var topBarLeftDialogVisible by remember { mutableStateOf(false) }
var topBarRightDialogVisible by remember { mutableStateOf(false) }
var bottomBarLeftDialogVisible by remember { mutableStateOf(false) }
var bottomBarRightDialogVisible by remember { mutableStateOf(false) }
var showAlerts by remember { mutableStateOf(false) } var showAlerts by remember { mutableStateOf(false) }
var givenPermissions by remember { mutableStateOf(false) } var givenPermissions by remember { mutableStateOf(false) }
@ -174,8 +160,6 @@ fun MainScreen(onFinish: () -> Unit) {
var customMaxPower by remember { mutableStateOf("") } var customMaxPower by remember { mutableStateOf("") }
var customMinHr by remember { mutableStateOf("") } var customMinHr by remember { mutableStateOf("") }
var customMaxHr by remember { mutableStateOf("") } var customMaxHr by remember { mutableStateOf("") }
var minGrade by remember { mutableStateOf("0") }
var maxGrade by remember { mutableStateOf("0") }
var useCustomPowerRange by remember { mutableStateOf(false) } var useCustomPowerRange by remember { mutableStateOf(false) }
var useCustomHrRange by remember { mutableStateOf(false) } var useCustomHrRange by remember { mutableStateOf(false) }
@ -193,13 +177,7 @@ fun MainScreen(onFinish: () -> Unit) {
val maxSpeedSetting = (maxSpeed.toIntOrNull()?.toFloat()?.div((if(isImperial) 2.23694f else 3.6f))) ?: PowerbarSettings.defaultMaxSpeedMs val maxSpeedSetting = (maxSpeed.toIntOrNull()?.toFloat()?.div((if(isImperial) 2.23694f else 3.6f))) ?: PowerbarSettings.defaultMaxSpeedMs
val newSettings = PowerbarSettings( val newSettings = PowerbarSettings(
bottomBarSource = bottomSelectedSource, topBarSource = topSelectedSource, source = bottomSelectedSource, topBarSource = topSelectedSource,
splitTopBar = splitTopBar,
splitBottomBar = splitBottomBar,
topBarLeftSource = topSelectedSourceLeft,
topBarRightSource = topSelectedSourceRight,
bottomBarLeftSource = bottomSelectedSourceLeft,
bottomBarRightSource = bottomSelectedSourceRight,
onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars, onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars,
barBackground = barBackground, barBackground = barBackground,
useZoneColors = colorBasedOnZones, useZoneColors = colorBasedOnZones,
@ -210,8 +188,6 @@ fun MainScreen(onFinish: () -> Unit) {
maxPower = customMaxPower.toIntOrNull(), maxPower = customMaxPower.toIntOrNull(),
minHr = customMinHr.toIntOrNull(), minHr = customMinHr.toIntOrNull(),
maxHr = customMaxHr.toIntOrNull(), maxHr = customMaxHr.toIntOrNull(),
minGradient = minGrade.toIntOrNull() ?: PowerbarSettings.defaultMinGradient,
maxGradient = maxGrade.toIntOrNull() ?: PowerbarSettings.defaultMaxGradient,
barBarSize = barBarSize, barBarSize = barBarSize,
barFontSize = barFontSize, barFontSize = barFontSize,
useCustomPowerRange = useCustomPowerRange, useCustomPowerRange = useCustomPowerRange,
@ -250,14 +226,8 @@ fun MainScreen(onFinish: () -> Unit) {
.combine(karooSystem.streamUserProfile()) { settings, profile -> settings to profile } .combine(karooSystem.streamUserProfile()) { settings, profile -> settings to profile }
.distinctUntilChanged() .distinctUntilChanged()
.collect { (settings, profile) -> .collect { (settings, profile) ->
bottomSelectedSource = settings.bottomBarSource bottomSelectedSource = settings.source
topSelectedSource = settings.topBarSource topSelectedSource = settings.topBarSource
splitTopBar = settings.splitTopBar
splitBottomBar = settings.splitBottomBar
topSelectedSourceLeft = settings.topBarLeftSource
topSelectedSourceRight = settings.topBarRightSource
bottomSelectedSourceLeft = settings.bottomBarLeftSource
bottomSelectedSourceRight = settings.bottomBarRightSource
onlyShowWhileRiding = settings.onlyShowWhileRiding onlyShowWhileRiding = settings.onlyShowWhileRiding
showLabelOnBars = settings.showLabelOnBars showLabelOnBars = settings.showLabelOnBars
colorBasedOnZones = settings.useZoneColors colorBasedOnZones = settings.useZoneColors
@ -273,8 +243,6 @@ fun MainScreen(onFinish: () -> Unit) {
customMaxPower = settings.maxPower?.toString() ?: "" customMaxPower = settings.maxPower?.toString() ?: ""
customMinHr = settings.minHr?.toString() ?: "" customMinHr = settings.minHr?.toString() ?: ""
customMaxHr = settings.maxHr?.toString() ?: "" customMaxHr = settings.maxHr?.toString() ?: ""
minGrade = settings.minGradient?.toString() ?: ""
maxGrade = settings.maxGradient?.toString() ?: ""
useCustomPowerRange = settings.useCustomPowerRange useCustomPowerRange = settings.useCustomPowerRange
useCustomHrRange = settings.useCustomHrRange useCustomHrRange = settings.useCustomHrRange
} }
@ -308,126 +276,6 @@ fun MainScreen(onFinish: () -> Unit) {
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) { .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp)) {
Text("Top Bar", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.weight(1f))
Text("Split")
Spacer(modifier = Modifier.width(10.dp))
Switch(checked = splitTopBar, onCheckedChange = {
splitTopBar = it
coroutineScope.launch { updateSettings() }
})
}
if (splitTopBar) {
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp),
onClick = {
topBarLeftDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Top Bar (Left): ${topSelectedSourceLeft.label}", modifier = Modifier.weight(1.0f))
}
if (topBarLeftDialogVisible){
BarSelectDialog(topSelectedSourceLeft, onHide = { topBarLeftDialogVisible = false }, onSelect = { selected ->
topSelectedSourceLeft = selected
coroutineScope.launch { updateSettings() }
topBarLeftDialogVisible = false
})
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp),
onClick = {
topBarRightDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Top Bar (Right): ${topSelectedSourceRight.label}", modifier = Modifier.weight(1.0f))
}
if (topBarRightDialogVisible){
BarSelectDialog(topSelectedSourceRight, onHide = { topBarRightDialogVisible = false }, onSelect = { selected ->
topSelectedSourceRight = selected
coroutineScope.launch { updateSettings() }
topBarRightDialogVisible = false
})
}
} else {
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp),
onClick = {
topBarDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Top Bar: ${topSelectedSource.label}", modifier = Modifier.weight(1.0f))
}
}
if (topBarDialogVisible){
BarSelectDialog(topSelectedSource, onHide = { topBarDialogVisible = false }, onSelect = { selected ->
topSelectedSource = selected
coroutineScope.launch { updateSettings() }
topBarDialogVisible = false
})
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp)) {
Text("Bottom Bar", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.weight(1f))
Text("Split")
Spacer(modifier = Modifier.width(10.dp))
Switch(checked = splitBottomBar, onCheckedChange = {
splitBottomBar = it
coroutineScope.launch { updateSettings() }
})
}
if (splitBottomBar) {
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp),
onClick = {
bottomBarLeftDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Bottom Bar (Left): ${bottomSelectedSourceLeft.label}", modifier = Modifier.weight(1.0f))
}
if (bottomBarLeftDialogVisible){
BarSelectDialog(bottomSelectedSourceLeft, onHide = { bottomBarLeftDialogVisible = false }, onSelect = { selected ->
bottomSelectedSourceLeft = selected
coroutineScope.launch { updateSettings() }
bottomBarLeftDialogVisible = false
})
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp),
onClick = {
bottomBarRightDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Bottom Bar (Right): ${bottomSelectedSourceRight.label}", modifier = Modifier.weight(1.0f))
}
if (bottomBarRightDialogVisible){
BarSelectDialog(bottomSelectedSourceRight, onHide = { bottomBarRightDialogVisible = false }, onSelect = { selected ->
bottomSelectedSourceRight = selected
coroutineScope.launch { updateSettings() }
bottomBarRightDialogVisible = false
})
}
} else {
FilledTonalButton(modifier = Modifier FilledTonalButton(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(60.dp), .height(60.dp),
@ -438,7 +286,6 @@ fun MainScreen(onFinish: () -> Unit) {
Spacer(modifier = Modifier.width(5.dp)) Spacer(modifier = Modifier.width(5.dp))
Text("Bottom Bar: ${bottomSelectedSource.label}", modifier = Modifier.weight(1.0f)) Text("Bottom Bar: ${bottomSelectedSource.label}", modifier = Modifier.weight(1.0f))
} }
}
if (bottomBarDialogVisible){ if (bottomBarDialogVisible){
BarSelectDialog(bottomSelectedSource, onHide = { bottomBarDialogVisible = false }, onSelect = { selected -> BarSelectDialog(bottomSelectedSource, onHide = { bottomBarDialogVisible = false }, onSelect = { selected ->
@ -448,6 +295,25 @@ fun MainScreen(onFinish: () -> Unit) {
}) })
} }
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp),
onClick = {
topBarDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Top Bar: ${topSelectedSource.label}", modifier = Modifier.weight(1.0f))
}
if (topBarDialogVisible){
BarSelectDialog(topSelectedSource, onHide = { topBarDialogVisible = false }, onSelect = { selected ->
topSelectedSource = selected
coroutineScope.launch { updateSettings() }
topBarDialogVisible = false
})
}
apply { apply {
val dropdownOptions = CustomProgressBarBarSize.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) } val dropdownOptions = CustomProgressBarBarSize.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val dropdownInitialSelection by remember(barBarSize) { val dropdownInitialSelection by remember(barBarSize) {
@ -471,17 +337,14 @@ fun MainScreen(onFinish: () -> Unit) {
} }
if (topSelectedSource == SelectedSource.SPEED || topSelectedSource == SelectedSource.SPEED_3S || if (topSelectedSource == SelectedSource.SPEED || topSelectedSource == SelectedSource.SPEED_3S ||
bottomSelectedSource == SelectedSource.SPEED || bottomSelectedSource == SelectedSource.SPEED_3S || bottomSelectedSource == SelectedSource.SPEED || bottomSelectedSource == SelectedSource.SPEED_3S){
(splitTopBar && (topSelectedSourceLeft == SelectedSource.SPEED || topSelectedSourceLeft == SelectedSource.SPEED_3S || topSelectedSourceRight == SelectedSource.SPEED || topSelectedSourceRight == SelectedSource.SPEED_3S)) ||
(splitBottomBar && (bottomSelectedSourceLeft == SelectedSource.SPEED || bottomSelectedSourceLeft == SelectedSource.SPEED_3S || bottomSelectedSourceRight == SelectedSource.SPEED || bottomSelectedSourceRight == SelectedSource.SPEED_3S))
){
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(value = minSpeed, modifier = Modifier OutlinedTextField(value = minSpeed, modifier = Modifier
.weight(1f) .weight(1f)
.absolutePadding(right = 2.dp) .absolutePadding(right = 2.dp)
.onFocusEvent(::updateFocus), .onFocusEvent(::updateFocus),
onValueChange = { minSpeed = it.filter { c -> c.isDigit() } }, onValueChange = { minSpeed = it },
label = { Text("Min Speed") }, label = { Text("Min Speed") },
suffix = { Text(if (isImperial) "mph" else "kph") }, suffix = { Text(if (isImperial) "mph" else "kph") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
@ -492,7 +355,7 @@ fun MainScreen(onFinish: () -> Unit) {
.weight(1f) .weight(1f)
.absolutePadding(left = 2.dp) .absolutePadding(left = 2.dp)
.onFocusEvent(::updateFocus), .onFocusEvent(::updateFocus),
onValueChange = { maxSpeed = it.filter { c -> c.isDigit() } }, onValueChange = { maxSpeed = it },
label = { Text("Max Speed") }, label = { Text("Max Speed") },
suffix = { Text(if (isImperial) "mph" else "kph") }, suffix = { Text(if (isImperial) "mph" else "kph") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
@ -501,10 +364,7 @@ fun MainScreen(onFinish: () -> Unit) {
} }
} }
if (topSelectedSource.isPower() || bottomSelectedSource.isPower() || if (topSelectedSource.isPower() || bottomSelectedSource.isPower()){
(splitTopBar && (topSelectedSourceLeft.isPower() || topSelectedSourceRight.isPower())) ||
(splitBottomBar && (bottomSelectedSourceLeft.isPower() || bottomSelectedSourceRight.isPower()))
){
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = useCustomPowerRange, onCheckedChange = { Switch(checked = useCustomPowerRange, onCheckedChange = {
useCustomPowerRange = it useCustomPowerRange = it
@ -520,7 +380,7 @@ fun MainScreen(onFinish: () -> Unit) {
.weight(1f) .weight(1f)
.absolutePadding(right = 2.dp) .absolutePadding(right = 2.dp)
.onFocusEvent(::updateFocus), .onFocusEvent(::updateFocus),
onValueChange = { customMinPower = it.filter { c -> c.isDigit() } }, onValueChange = { customMinPower = it },
label = { Text("Min Power", fontSize = 12.sp) }, label = { Text("Min Power", fontSize = 12.sp) },
suffix = { Text("W") }, suffix = { Text("W") },
placeholder = { Text("$profileMinPower") }, placeholder = { Text("$profileMinPower") },
@ -532,7 +392,7 @@ fun MainScreen(onFinish: () -> Unit) {
.weight(1f) .weight(1f)
.absolutePadding(left = 2.dp) .absolutePadding(left = 2.dp)
.onFocusEvent(::updateFocus), .onFocusEvent(::updateFocus),
onValueChange = { customMaxPower = it.filter { c -> c.isDigit() } }, onValueChange = { customMaxPower = it },
label = { Text("Max Power", fontSize = 12.sp) }, label = { Text("Max Power", fontSize = 12.sp) },
suffix = { Text("W") }, suffix = { Text("W") },
placeholder = { Text("$profileMaxPower") }, placeholder = { Text("$profileMaxPower") },
@ -543,10 +403,7 @@ fun MainScreen(onFinish: () -> Unit) {
} }
} }
if (topSelectedSource == SelectedSource.HEART_RATE || bottomSelectedSource == SelectedSource.HEART_RATE || if (topSelectedSource == SelectedSource.HEART_RATE || bottomSelectedSource == SelectedSource.HEART_RATE){
(splitTopBar && (topSelectedSourceLeft == SelectedSource.HEART_RATE || topSelectedSourceRight == SelectedSource.HEART_RATE)) ||
(splitBottomBar && (bottomSelectedSourceLeft == SelectedSource.HEART_RATE || bottomSelectedSourceRight == SelectedSource.HEART_RATE))
){
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = useCustomHrRange, onCheckedChange = { Switch(checked = useCustomHrRange, onCheckedChange = {
useCustomHrRange = it useCustomHrRange = it
@ -562,7 +419,7 @@ fun MainScreen(onFinish: () -> Unit) {
.weight(1f) .weight(1f)
.absolutePadding(right = 2.dp) .absolutePadding(right = 2.dp)
.onFocusEvent(::updateFocus), .onFocusEvent(::updateFocus),
onValueChange = { customMinHr = it.filter { c -> c.isDigit() } }, onValueChange = { customMinHr = it },
label = { Text("Min Hr") }, label = { Text("Min Hr") },
suffix = { Text("bpm") }, suffix = { Text("bpm") },
placeholder = { if(profileRestHr > 0) Text("$profileRestHr") else Unit }, placeholder = { if(profileRestHr > 0) Text("$profileRestHr") else Unit },
@ -574,7 +431,7 @@ fun MainScreen(onFinish: () -> Unit) {
.weight(1f) .weight(1f)
.absolutePadding(left = 2.dp) .absolutePadding(left = 2.dp)
.onFocusEvent(::updateFocus), .onFocusEvent(::updateFocus),
onValueChange = { customMaxHr = it.filter { c -> c.isDigit() } }, onValueChange = { customMaxHr = it },
label = { Text("Max Hr") }, label = { Text("Max Hr") },
suffix = { Text("bpm") }, suffix = { Text("bpm") },
placeholder = { if(profileMaxHr > 0) Text("$profileMaxHr") else Unit }, placeholder = { if(profileMaxHr > 0) Text("$profileMaxHr") else Unit },
@ -586,17 +443,14 @@ fun MainScreen(onFinish: () -> Unit) {
} }
if (bottomSelectedSource == SelectedSource.CADENCE || topSelectedSource == SelectedSource.CADENCE || if (bottomSelectedSource == SelectedSource.CADENCE || topSelectedSource == SelectedSource.CADENCE ||
bottomSelectedSource == SelectedSource.CADENCE_3S || topSelectedSource == SelectedSource.CADENCE_3S || bottomSelectedSource == SelectedSource.CADENCE_3S || topSelectedSource == SelectedSource.CADENCE_3S){
(splitTopBar && (topSelectedSourceLeft == SelectedSource.CADENCE || topSelectedSourceLeft == SelectedSource.CADENCE_3S || topSelectedSourceRight == SelectedSource.CADENCE || topSelectedSourceRight == SelectedSource.CADENCE_3S)) ||
(splitBottomBar && (bottomSelectedSourceLeft == SelectedSource.CADENCE || bottomSelectedSourceLeft == SelectedSource.CADENCE_3S || bottomSelectedSourceRight == SelectedSource.CADENCE || bottomSelectedSourceRight == SelectedSource.CADENCE_3S))
){
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(value = minCadence, modifier = Modifier OutlinedTextField(value = minCadence, modifier = Modifier
.weight(1f) .weight(1f)
.absolutePadding(right = 2.dp) .absolutePadding(right = 2.dp)
.onFocusEvent(::updateFocus), .onFocusEvent(::updateFocus),
onValueChange = { minCadence = it.filter { c -> c.isDigit() } }, onValueChange = { minCadence = it },
label = { Text("Min Cadence") }, label = { Text("Min Cadence") },
suffix = { Text("rpm") }, suffix = { Text("rpm") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
@ -607,7 +461,7 @@ fun MainScreen(onFinish: () -> Unit) {
.weight(1f) .weight(1f)
.absolutePadding(left = 2.dp) .absolutePadding(left = 2.dp)
.onFocusEvent(::updateFocus), .onFocusEvent(::updateFocus),
onValueChange = { maxCadence = it.filter { c -> c.isDigit() } }, onValueChange = { maxCadence = it },
label = { Text("Min Cadence") }, label = { Text("Min Cadence") },
suffix = { Text("rpm") }, suffix = { Text("rpm") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
@ -616,35 +470,6 @@ fun MainScreen(onFinish: () -> Unit) {
} }
} }
if (topSelectedSource == SelectedSource.GRADE || bottomSelectedSource == SelectedSource.GRADE ||
(splitTopBar && (topSelectedSourceLeft == SelectedSource.GRADE || topSelectedSourceRight == SelectedSource.GRADE)) ||
(splitBottomBar && (bottomSelectedSourceLeft == SelectedSource.GRADE || bottomSelectedSourceRight == SelectedSource.GRADE))
){
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(value = minGrade, modifier = Modifier
.weight(1f)
.absolutePadding(right = 2.dp)
.onFocusEvent(::updateFocus),
onValueChange = { minGrade = it.filterIndexed { index, c -> c.isDigit() || (c == '-' && index == 0) } },
label = { Text("Min Grade") },
suffix = { Text("%") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true
)
OutlinedTextField(value = maxGrade, modifier = Modifier
.weight(1f)
.absolutePadding(left = 2.dp)
.onFocusEvent(::updateFocus),
onValueChange = { maxGrade = it.filterIndexed { index, c -> c.isDigit() || (c == '-' && index == 0) } },
label = { Text("Max Grade") },
suffix = { Text("%") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = colorBasedOnZones, onCheckedChange = { Switch(checked = colorBasedOnZones, onCheckedChange = {
colorBasedOnZones = it colorBasedOnZones = it
@ -669,7 +494,7 @@ fun MainScreen(onFinish: () -> Unit) {
coroutineScope.launch { updateSettings() } coroutineScope.launch { updateSettings() }
}) })
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text("Solid background") Text("Opaque background")
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {

View File

@ -4,8 +4,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp"> android:layout_height="80dp">
<de.timklge.karoopowerbar.CustomView <de.timklge.karoopowerbar.CustomProgressBar
android:id="@+id/customView" android:id="@+id/progressBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="80dp"
android:layout_gravity="center" /> android:layout_gravity="center" />

View File

@ -12,17 +12,4 @@
<color name="zone6">#FE581F</color> <color name="zone6">#FE581F</color>
<color name="zone7">#D60404</color> <color name="zone7">#D60404</color>
<color name="zone8">#B700A2</color> <color name="zone8">#B700A2</color>
<color name="eleDarkGreen">#079d78</color>
<color name="eleLightGreen">#58c597</color>
<color name="eleYellow">#e7e021</color>
<color name="eleLightOrange">#e59174</color>
<color name="eleDarkOrange">#e7693a</color>
<color name="eleRed">#c82425</color>
<color name="eleLightRed">#f05a28</color>
<color name="elePurple">#b222a3</color>
<color name="eleWhite">#ffffff</color>
<color name="eleGray">#9e9e9e</color>
<color name="eleLightBlue">#4fc3f7</color>
<color name="eleDarkBlue">#2D58AF</color>
</resources> </resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB