Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa0f3c931e | |||
|
|
d30bcb7abe | ||
|
|
34c87f68ab | ||
| b72180ee27 | |||
|
|
97fadc742e | ||
|
|
304a984110 | ||
|
|
f25ab5d7db | ||
| f99a82ec74 | |||
|
|
2d7947b6a2 | ||
|
|
31418b834d | ||
|
|
c202f20320 | ||
|
|
1afeaae9e6 | ||
|
|
b8c3356674 | ||
|
|
807623d04a |
6
.github/workflows/android.yml
vendored
6
.github/workflows/android.yml
vendored
@ -45,6 +45,12 @@ jobs:
|
|||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build
|
run: ./gradlew build
|
||||||
|
|
||||||
|
- name: Archive APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app-release.apk
|
||||||
|
path: app/build/outputs/apk/release/app-release.apk
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
|||||||
@ -12,6 +12,7 @@ Compatible with Karoo 2 and Karoo 3 devices.
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -25,6 +26,9 @@ 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.
|
||||||
|
|||||||
@ -72,11 +72,13 @@ 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 route progress data source\n* Add workout target range indicator\n* Make bars transparent by default\n* Split size setting",
|
"releaseNotes" to "* Show zero value on bars to indicate sensor availability\n* Fix pedal balance values\n* Add pedal balance data source\n* Add option to split bars",
|
||||||
"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",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -8,15 +8,33 @@ 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 CustomProgressBar @JvmOverloads constructor(
|
class CustomView @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
|
||||||
@ -24,12 +42,13 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
var showLabel: Boolean = true
|
var showLabel: Boolean = true
|
||||||
var barBackground: Boolean = false
|
var barBackground: Boolean = false
|
||||||
@ColorInt var progressColor: Int = 0xFF2b86e6.toInt()
|
@ColorInt var progressColor: Int = 0xFF2b86e6.toInt()
|
||||||
|
var drawMode: ProgressBarDrawMode = ProgressBarDrawMode.STANDARD
|
||||||
|
|
||||||
var fontSize = CustomProgressBarFontSize.MEDIUM
|
var fontSize = CustomProgressBarFontSize.MEDIUM
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
textPaint.textSize = value.fontSize
|
textPaint.textSize = value.fontSize
|
||||||
invalidate() // Redraw to apply new font size
|
view.invalidate() // Redraw to apply new font size
|
||||||
}
|
}
|
||||||
|
|
||||||
var barSize = CustomProgressBarBarSize.MEDIUM
|
var barSize = CustomProgressBarBarSize.MEDIUM
|
||||||
@ -45,7 +64,7 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
CustomProgressBarBarSize.MEDIUM -> 8f
|
CustomProgressBarBarSize.MEDIUM -> 8f
|
||||||
CustomProgressBarBarSize.LARGE -> 10f
|
CustomProgressBarBarSize.LARGE -> 10f
|
||||||
}
|
}
|
||||||
invalidate() // Redraw to apply new bar size
|
view.invalidate() // Redraw to apply new bar size
|
||||||
}
|
}
|
||||||
|
|
||||||
private val targetColor = 0xFF9933FF.toInt()
|
private val targetColor = 0xFF9933FF.toInt()
|
||||||
@ -120,9 +139,7 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
style = Paint.Style.STROKE
|
style = Paint.Style.STROKE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDrawForeground(canvas: Canvas) {
|
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!!
|
||||||
@ -132,25 +149,113 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
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
|
||||||
|
|
||||||
|
// Calculate bar left and right positions based on draw mode
|
||||||
|
val (barLeft, barRight) = when (drawMode) {
|
||||||
|
ProgressBarDrawMode.STANDARD -> {
|
||||||
|
// Standard left-to-right progress bar
|
||||||
|
when (horizontalLocation) {
|
||||||
|
HorizontalPowerbarLocation.LEFT -> Pair(0f, (halfWidth * p).toFloat())
|
||||||
|
HorizontalPowerbarLocation.RIGHT -> Pair(fullWidth - (halfWidth * p).toFloat(), fullWidth)
|
||||||
|
HorizontalPowerbarLocation.FULL -> Pair(0f, (fullWidth * p).toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProgressBarDrawMode.CENTER_OUT -> {
|
||||||
|
// Center-outward progress bar: 0.5 = invisible, <0.5 = extend left, >0.5 = extend right
|
||||||
|
when (horizontalLocation) {
|
||||||
|
HorizontalPowerbarLocation.LEFT -> {
|
||||||
|
val centerPoint = halfWidth / 2f // Center of left half
|
||||||
|
when {
|
||||||
|
p == 0.5 -> Pair(centerPoint, centerPoint) // Invisible at 0.5
|
||||||
|
p < 0.5 -> {
|
||||||
|
val leftExtent = centerPoint * (0.5 - p) * 2.0 // Map 0.0-0.5 to full left extension
|
||||||
|
Pair((centerPoint - leftExtent).toFloat(), centerPoint)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val rightExtent = centerPoint * (p - 0.5) * 2.0 // Map 0.5-1.0 to full right extension
|
||||||
|
Pair(centerPoint, (centerPoint + rightExtent).toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalPowerbarLocation.RIGHT -> {
|
||||||
|
val centerPoint = halfWidth + (halfWidth / 2f) // Center of right half
|
||||||
|
when {
|
||||||
|
p == 0.5 -> Pair(centerPoint, centerPoint) // Invisible at 0.5
|
||||||
|
p < 0.5 -> {
|
||||||
|
val leftExtent = (halfWidth / 2f) * (0.5 - p) * 2.0 // Map 0.0-0.5 to full left extension
|
||||||
|
Pair((centerPoint - leftExtent).toFloat(), centerPoint)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val rightExtent = (halfWidth / 2f) * (p - 0.5) * 2.0 // Map 0.5-1.0 to full right extension
|
||||||
|
Pair(centerPoint, (centerPoint + rightExtent).toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalPowerbarLocation.FULL -> {
|
||||||
|
val centerPoint = halfWidth // Center of full width
|
||||||
|
when {
|
||||||
|
p == 0.5 -> Pair(centerPoint, centerPoint) // Invisible at 0.5
|
||||||
|
p < 0.5 -> {
|
||||||
|
val leftExtent = halfWidth * (0.5 - p) * 2.0 // Map 0.0-0.5 to full left extension
|
||||||
|
Pair((centerPoint - leftExtent).toFloat(), centerPoint)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val rightExtent = halfWidth * (p - 0.5) * 2.0 // Map 0.5-1.0 to full right extension
|
||||||
|
Pair(centerPoint, (centerPoint + rightExtent).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(
|
||||||
1f,
|
barLeft,
|
||||||
15f,
|
15f,
|
||||||
((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(0.0, 1.0)).toFloat(),
|
barRight,
|
||||||
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(0f, 15f, canvas.width.toFloat(), 15f + barSize.barHeight, backgroundPaint)
|
canvas.drawRect(backgroundLeft, 15f, backgroundRight, 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,
|
||||||
@ -172,8 +277,6 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
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,
|
||||||
@ -188,7 +291,6 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
|
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
@ -203,7 +305,44 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
|
|
||||||
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 = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f)
|
|
||||||
|
// Calculate label position based on draw mode
|
||||||
|
val x = when (drawMode) {
|
||||||
|
ProgressBarDrawMode.STANDARD -> {
|
||||||
|
// Original logic for standard mode
|
||||||
|
(if (horizontalLocation != HorizontalPowerbarLocation.RIGHT) rect.right - xOffset else rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
ProgressBarDrawMode.CENTER_OUT -> {
|
||||||
|
// For center outward mode, position label at the edge of the bar
|
||||||
|
when {
|
||||||
|
p == 0.5 -> {
|
||||||
|
// When bar is invisible (at center), position at center
|
||||||
|
when (horizontalLocation) {
|
||||||
|
HorizontalPowerbarLocation.LEFT -> {
|
||||||
|
val centerPoint = halfWidth / 2f
|
||||||
|
(centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
HorizontalPowerbarLocation.RIGHT -> {
|
||||||
|
val centerPoint = halfWidth + (halfWidth / 2f)
|
||||||
|
(centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
HorizontalPowerbarLocation.FULL -> {
|
||||||
|
val centerPoint = halfWidth
|
||||||
|
(centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p < 0.5 -> {
|
||||||
|
// Bar extends left from center, place label at left edge
|
||||||
|
(rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Bar extends right from center, place label at right edge
|
||||||
|
(rect.right - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
val r = x + xOffset * 2
|
val r = x + xOffset * 2
|
||||||
|
|
||||||
val fm = textPaint.fontMetrics
|
val fm = textPaint.fontMetrics
|
||||||
@ -226,9 +365,9 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
|
|
||||||
PowerbarLocation.BOTTOM -> {
|
PowerbarLocation.BOTTOM -> {
|
||||||
val rect = RectF(
|
val rect = RectF(
|
||||||
1f,
|
barLeft,
|
||||||
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
|
||||||
((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(0.0, 1.0)).toFloat(),
|
barRight,
|
||||||
canvas.height.toFloat()
|
canvas.height.toFloat()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -236,13 +375,11 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
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(0f, canvas.height.toFloat() - barSize.barHeight, canvas.width.toFloat(), canvas.height.toFloat(), backgroundPaint)
|
canvas.drawRect(backgroundLeft, canvas.height.toFloat() - barSize.barHeight, backgroundRight, 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,
|
||||||
@ -265,8 +402,6 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
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,
|
||||||
@ -281,7 +416,6 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
|
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
@ -296,7 +430,44 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
|
|
||||||
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 = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f)
|
|
||||||
|
// Calculate label position based on draw mode
|
||||||
|
val x = when (drawMode) {
|
||||||
|
ProgressBarDrawMode.STANDARD -> {
|
||||||
|
// Original logic for standard mode
|
||||||
|
(if (horizontalLocation != HorizontalPowerbarLocation.RIGHT) rect.right - xOffset else rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
ProgressBarDrawMode.CENTER_OUT -> {
|
||||||
|
// For center outward mode, position label at the edge of the bar
|
||||||
|
when {
|
||||||
|
p == 0.5 -> {
|
||||||
|
// When bar is invisible (at center), position at center
|
||||||
|
when (horizontalLocation) {
|
||||||
|
HorizontalPowerbarLocation.LEFT -> {
|
||||||
|
val centerPoint = halfWidth / 2f
|
||||||
|
(centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
HorizontalPowerbarLocation.RIGHT -> {
|
||||||
|
val centerPoint = halfWidth + (halfWidth / 2f)
|
||||||
|
(centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
HorizontalPowerbarLocation.FULL -> {
|
||||||
|
val centerPoint = halfWidth
|
||||||
|
(centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p < 0.5 -> {
|
||||||
|
// Bar extends left from center, place label at left edge
|
||||||
|
(rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Bar extends right from center, place label at right edge
|
||||||
|
(rect.right - xOffset).coerceIn(backgroundLeft..backgroundRight-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.
|
||||||
@ -315,4 +486,9 @@ class CustomProgressBar @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun invalidate() {
|
||||||
|
// Invalidate the view to trigger a redraw
|
||||||
|
view.invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,17 +52,18 @@ class ForegroundService : Service() {
|
|||||||
windows.forEach { it.close() }
|
windows.forEach { it.close() }
|
||||||
windows.clear()
|
windows.clear()
|
||||||
|
|
||||||
if (settings.source != SelectedSource.NONE && showBars) {
|
if (showBars){
|
||||||
Window(this@ForegroundService, PowerbarLocation.BOTTOM, settings.showLabelOnBars, settings.barBackground, settings.barBarSize, settings.barFontSize).apply {
|
if (settings.bottomBarSource != SelectedSource.NONE || settings.bottomBarLeftSource != SelectedSource.NONE || settings.bottomBarRightSource != SelectedSource.NONE) {
|
||||||
selectedSource = settings.source
|
Window(this@ForegroundService, PowerbarLocation.BOTTOM, settings.showLabelOnBars, settings.barBackground, settings.barBarSize, settings.barFontSize,
|
||||||
|
settings.splitBottomBar, settings.bottomBarSource, settings.bottomBarLeftSource, settings.bottomBarRightSource).apply {
|
||||||
windows.add(this)
|
windows.add(this)
|
||||||
open()
|
open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.topBarSource != SelectedSource.NONE && showBars){
|
if (settings.topBarSource != SelectedSource.NONE || settings.topBarLeftSource != SelectedSource.NONE || settings.topBarRightSource != SelectedSource.NONE) {
|
||||||
Window(this@ForegroundService, PowerbarLocation.TOP, settings.showLabelOnBars, settings.barBackground, settings.barBarSize, settings.barFontSize).apply {
|
Window(this@ForegroundService, PowerbarLocation.TOP, settings.showLabelOnBars, settings.barBackground, settings.barBarSize, settings.barFontSize,
|
||||||
selectedSource = settings.topBarSource
|
settings.splitTopBar, settings.topBarSource, settings.topBarLeftSource, settings.topBarRightSource).apply {
|
||||||
open()
|
open()
|
||||||
windows.add(this)
|
windows.add(this)
|
||||||
}
|
}
|
||||||
@ -70,6 +71,7 @@ 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)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ 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
|
||||||
@ -15,8 +16,16 @@ val settingsKey = stringPreferencesKey("settings")
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PowerbarSettings(
|
data class PowerbarSettings(
|
||||||
val source: SelectedSource = SelectedSource.POWER,
|
@SerialName("source") val bottomBarSource: 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,
|
||||||
@ -29,7 +38,11 @@ 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 useCustomHrRange: Boolean = false, val useCustomPowerRange: Boolean = false
|
val minGradient: Int? = defaultMinGradient, val maxGradient: Int? = defaultMaxGradient,
|
||||||
|
|
||||||
|
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())
|
||||||
@ -37,6 +50,8 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,10 +34,11 @@ 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.absoluteValue
|
||||||
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,6 +51,15 @@ enum class PowerbarLocation {
|
|||||||
TOP, BOTTOM
|
TOP, BOTTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class HorizontalPowerbarLocation {
|
||||||
|
FULL, LEFT, RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ProgressBarDrawMode {
|
||||||
|
STANDARD, // Normal left-to-right progress
|
||||||
|
CENTER_OUT // Progress extends outward from center (0.5 = invisible, <0.5 = left, >0.5 = right)
|
||||||
|
}
|
||||||
|
|
||||||
class Window(
|
class Window(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM,
|
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM,
|
||||||
@ -57,6 +67,10 @@ 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";
|
||||||
@ -69,9 +83,8 @@ class Window(
|
|||||||
private val windowManager: WindowManager
|
private val windowManager: WindowManager
|
||||||
private val layoutInflater: LayoutInflater
|
private val layoutInflater: LayoutInflater
|
||||||
|
|
||||||
private val powerbar: CustomProgressBar
|
private val powerbars: MutableMap<HorizontalPowerbarLocation, CustomProgressBar> = mutableMapOf()
|
||||||
|
private val view: CustomView
|
||||||
var selectedSource: SelectedSource = SelectedSource.POWER
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
layoutParams = WindowManager.LayoutParams(
|
layoutParams = WindowManager.LayoutParams(
|
||||||
@ -84,8 +97,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)
|
||||||
powerbar = rootView.findViewById(R.id.progressBar)
|
view = rootView.findViewById(R.id.customView)
|
||||||
powerbar.progress = null
|
view.progressBars = powerbars
|
||||||
|
|
||||||
windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
|
windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
val displayMetrics = DisplayMetrics()
|
val displayMetrics = DisplayMetrics()
|
||||||
@ -116,11 +129,10 @@ class Window(
|
|||||||
|
|
||||||
private val karooSystem: KarooSystemService = KarooSystemService(context)
|
private val karooSystem: KarooSystemService = KarooSystemService(context)
|
||||||
|
|
||||||
private var serviceJob: Job? = null
|
private var serviceJobs: MutableSet<Job> = mutableSetOf()
|
||||||
|
|
||||||
@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)
|
||||||
@ -132,29 +144,54 @@ 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(PowerStreamSmoothing.RAW)
|
SelectedSource.POWER -> streamPower(SelectedSource.POWER, PowerStreamSmoothing.RAW)
|
||||||
SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S)
|
SelectedSource.POWER_3S -> streamPower(SelectedSource.POWER_3S, PowerStreamSmoothing.SMOOTHED_3S)
|
||||||
SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S)
|
SelectedSource.POWER_10S -> streamPower(SelectedSource.POWER_10S, PowerStreamSmoothing.SMOOTHED_10S)
|
||||||
SelectedSource.HEART_RATE -> streamHeartrate()
|
SelectedSource.HEART_RATE -> streamHeartrate()
|
||||||
SelectedSource.SPEED -> streamSpeed(false)
|
SelectedSource.SPEED -> streamSpeed(SelectedSource.SPEED, false)
|
||||||
SelectedSource.SPEED_3S -> streamSpeed(true)
|
SelectedSource.SPEED_3S -> streamSpeed(SelectedSource.SPEED_3S, true)
|
||||||
SelectedSource.CADENCE -> streamCadence(false)
|
SelectedSource.CADENCE -> streamCadence(SelectedSource.CADENCE, false)
|
||||||
SelectedSource.CADENCE_3S -> streamCadence(true)
|
SelectedSource.CADENCE_3S -> streamCadence(SelectedSource.CADENCE_3S, true)
|
||||||
SelectedSource.ROUTE_PROGRESS -> streamRouteProgress()
|
SelectedSource.ROUTE_PROGRESS -> streamRouteProgress(SelectedSource.ROUTE_PROGRESS, ::getRouteProgress)
|
||||||
else -> {}
|
SelectedSource.REMAINING_ROUTE -> streamRouteProgress(SelectedSource.REMAINING_ROUTE, ::getRemainingRouteProgress)
|
||||||
|
SelectedSource.GRADE -> streamGrade()
|
||||||
|
SelectedSource.POWER_BALANCE -> streamBalance()
|
||||||
|
SelectedSource.NONE -> {}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -168,19 +205,103 @@ class Window(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun streamRouteProgress() {
|
private suspend fun streamBalance() {
|
||||||
|
data class StreamData(val powerBalanceLeft: Double?, val power: Double?)
|
||||||
|
|
||||||
|
karooSystem.streamDataFlow(DataType.Type.PEDAL_POWER_BALANCE)
|
||||||
|
.map {
|
||||||
|
val values = (it as? StreamState.Streaming)?.dataPoint?.values
|
||||||
|
|
||||||
|
StreamData(values?.get(DataType.Field.PEDAL_POWER_BALANCE_LEFT), values?.get(DataType.Field.POWER))
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.throttle(1_000).collect { streamData ->
|
||||||
|
val powerBalanceLeft = streamData.powerBalanceLeft
|
||||||
|
val powerbarsWithBalanceSource = powerbars.values.filter { it.source == SelectedSource.POWER_BALANCE }
|
||||||
|
|
||||||
|
powerbarsWithBalanceSource.forEach { powerbar ->
|
||||||
|
powerbar.drawMode = ProgressBarDrawMode.CENTER_OUT
|
||||||
|
|
||||||
|
if (streamData.powerBalanceLeft != null) {
|
||||||
|
val value = remap((powerBalanceLeft ?: 50.0).coerceIn(0.0, 100.0), 40.0, 60.0, 0.0, 100.0)
|
||||||
|
|
||||||
|
val percentLeft = (powerBalanceLeft ?: 50.0).roundToInt()
|
||||||
|
|
||||||
|
@ColorRes val zoneColorRes = if (percentLeft < 50) {
|
||||||
|
R.color.zone0
|
||||||
|
} else if (percentLeft == 50) {
|
||||||
|
R.color.zone1
|
||||||
|
} else {
|
||||||
|
R.color.zone7
|
||||||
|
}
|
||||||
|
|
||||||
|
powerbar.progressColor = context.getColor(zoneColorRes)
|
||||||
|
powerbar.progress = value?.div(100.0)
|
||||||
|
|
||||||
|
val percentRight = 100 - percentLeft
|
||||||
|
|
||||||
|
powerbar.label = "${percentLeft}-${percentRight}"
|
||||||
|
|
||||||
|
Log.d(TAG, "Balance: $powerBalanceLeft power: ${streamData.power}")
|
||||||
|
} else {
|
||||||
|
powerbar.progressColor = context.getColor(R.color.zone0)
|
||||||
|
powerbar.progress = null
|
||||||
|
powerbar.label = "?"
|
||||||
|
|
||||||
|
Log.d(TAG, "Balance: Unavailable")
|
||||||
|
}
|
||||||
|
powerbar.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BarProgress(
|
||||||
|
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?.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) { userProfile, distanceToDestination, navigationState ->
|
combine(karooSystem.streamUserProfile(), karooSystem.streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION), karooSystem.streamNavigationState(), karooSystem.streamDataFlow(DataType.Type.DISTANCE)) { userProfile, distanceToDestination, navigationState, riddenDistance ->
|
||||||
StreamData(userProfile, (distanceToDestination as? StreamState.Streaming)?.dataPoint?.values[DataType.Field.DISTANCE_TO_DESTINATION], navigationState)
|
StreamData(
|
||||||
}.distinctUntilChanged().throttle(5_000).collect { (userProfile, distanceToDestination, navigationState) ->
|
userProfile,
|
||||||
|
(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
|
||||||
@ -202,22 +323,21 @@ class Window(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val routeLength = lastKnownRouteLength
|
val routeEndAt = lastKnownRouteLength?.plus((distanceToDestination ?: 0.0))
|
||||||
val routeProgressMeters = routeLength?.let { routeLength - (distanceToDestination ?: 0.0) }?.coerceAtLeast(0.0)
|
val barProgress = routeProgressProvider(userProfile, riddenDistance, routeEndAt, distanceToDestination)
|
||||||
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 = routeProgress
|
powerbar.progress = barProgress.progress
|
||||||
powerbar.label = "$routeProgressInUserUnit"
|
powerbar.label = barProgress.label ?: ""
|
||||||
powerbar.invalidate()
|
powerbar.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun streamSpeed(smoothed: Boolean) {
|
private suspend fun streamSpeed(source: SelectedSource, 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()
|
||||||
@ -235,7 +355,9 @@ class Window(
|
|||||||
else -> valueMetersPerSecond?.times(3.6)
|
else -> valueMetersPerSecond?.times(3.6)
|
||||||
}?.roundToInt()
|
}?.roundToInt()
|
||||||
|
|
||||||
if (value != null && valueMetersPerSecond != null) {
|
val powerbarsWithSpeedSource = powerbars.values.filter { it.source == source }
|
||||||
|
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
|
||||||
@ -247,7 +369,7 @@ class Window(
|
|||||||
} else {
|
} else {
|
||||||
context.getColor(R.color.zone0)
|
context.getColor(R.color.zone0)
|
||||||
}
|
}
|
||||||
powerbar.progress = if (value > 0) progress else null
|
powerbar.progress = progress
|
||||||
powerbar.label = "$value"
|
powerbar.label = "$value"
|
||||||
|
|
||||||
Log.d(TAG, "Speed: $value min: $minSpeed max: $maxSpeed")
|
Log.d(TAG, "Speed: $value min: $minSpeed max: $maxSpeed")
|
||||||
@ -261,8 +383,67 @@ class Window(
|
|||||||
powerbar.invalidate()
|
powerbar.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun streamCadence(smoothed: Boolean) {
|
private suspend fun streamGrade() {
|
||||||
|
@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
|
||||||
|
|
||||||
|
powerbar.progress = remap(value.absoluteValue, minGradient.toDouble(), maxGradient.toDouble(), 0.0, 1.0)
|
||||||
|
|
||||||
|
val colorRes = getInclineIndicatorColor(value.toFloat()) ?: R.color.zone0
|
||||||
|
powerbar.progressColor = context.getColor(colorRes)
|
||||||
|
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()
|
||||||
@ -278,15 +459,17 @@ 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[FIELD_TARGET_MIN_ID]?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0)
|
powerbar.minTarget = remap(streamData.cadenceTarget?.values?.get(FIELD_TARGET_MIN_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.maxTarget = remap(streamData.cadenceTarget?.values?.get(FIELD_TARGET_MAX_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)
|
powerbar.target = remap(streamData.cadenceTarget?.values?.get(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
|
||||||
|
|
||||||
@ -295,7 +478,7 @@ class Window(
|
|||||||
} else {
|
} else {
|
||||||
context.getColor(R.color.zone0)
|
context.getColor(R.color.zone0)
|
||||||
}
|
}
|
||||||
powerbar.progress = if (value > 0) progress else null
|
powerbar.progress = progress
|
||||||
powerbar.label = "$value"
|
powerbar.label = "$value"
|
||||||
|
|
||||||
Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence")
|
Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence")
|
||||||
@ -309,6 +492,7 @@ 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)
|
||||||
@ -326,7 +510,9 @@ 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
|
||||||
@ -334,16 +520,16 @@ 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[FIELD_TARGET_MIN_ID]?.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.maxTarget = remap(streamData.heartrateTarget?.values[FIELD_TARGET_MAX_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.target = remap(streamData.heartrateTarget?.values[FIELD_TARGET_VALUE_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.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)
|
||||||
} else {
|
} else {
|
||||||
context.getColor(R.color.zone0)
|
context.getColor(R.color.zone0)
|
||||||
}
|
}
|
||||||
powerbar.progress = if (value > 0) progress else null
|
powerbar.progress = progress
|
||||||
powerbar.label = "$value"
|
powerbar.label = "$value"
|
||||||
|
|
||||||
Log.d(TAG, "Hr: $value min: $minHr max: $maxHr")
|
Log.d(TAG, "Hr: $value min: $minHr max: $maxHr")
|
||||||
@ -357,6 +543,7 @@ 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),
|
||||||
@ -364,7 +551,7 @@ class Window(
|
|||||||
SMOOTHED_10S(DataType.Type.SMOOTHED_10S_AVERAGE_POWER),
|
SMOOTHED_10S(DataType.Type.SMOOTHED_10S_AVERAGE_POWER),
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun streamPower(smoothed: PowerStreamSmoothing) {
|
private suspend fun streamPower(source: SelectedSource, 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()
|
||||||
@ -381,7 +568,9 @@ 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
|
||||||
@ -389,16 +578,16 @@ 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[FIELD_TARGET_MIN_ID]?.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.maxTarget = remap(streamData.powerTarget?.values[FIELD_TARGET_MAX_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.target = remap(streamData.powerTarget?.values[FIELD_TARGET_VALUE_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.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)
|
||||||
} else {
|
} else {
|
||||||
context.getColor(R.color.zone0)
|
context.getColor(R.color.zone0)
|
||||||
}
|
}
|
||||||
powerbar.progress = if (value > 0) progress else null
|
powerbar.progress = progress
|
||||||
powerbar.label = "${value}W"
|
powerbar.label = "${value}W"
|
||||||
|
|
||||||
Log.d(TAG, "Power: $value min: $minPower max: $maxPower")
|
Log.d(TAG, "Power: $value min: $minPower max: $maxPower")
|
||||||
@ -412,6 +601,7 @@ class Window(
|
|||||||
powerbar.invalidate()
|
powerbar.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var currentHideJob: Job? = null
|
private var currentHideJob: Job? = null
|
||||||
|
|
||||||
@ -422,7 +612,9 @@ class Window(
|
|||||||
currentHideJob?.cancel()
|
currentHideJob?.cancel()
|
||||||
currentHideJob = null
|
currentHideJob = null
|
||||||
}
|
}
|
||||||
serviceJob?.cancel()
|
serviceJobs.forEach { job ->
|
||||||
|
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()
|
||||||
|
|||||||
@ -59,7 +59,6 @@ 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
|
||||||
@ -85,10 +84,13 @@ 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)"),
|
||||||
ROUTE_PROGRESS("route_progress", "Route Progress");
|
GRADE("grade", "Grade"),
|
||||||
|
POWER_BALANCE("power_balance", "Power Balance"),
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@ -138,9 +140,22 @@ 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) }
|
||||||
|
|
||||||
@ -160,6 +175,8 @@ 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) }
|
||||||
|
|
||||||
@ -177,7 +194,13 @@ 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(
|
||||||
source = bottomSelectedSource, topBarSource = topSelectedSource,
|
bottomBarSource = 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,
|
||||||
@ -188,6 +211,8 @@ 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,
|
||||||
@ -226,8 +251,14 @@ 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.source
|
bottomSelectedSource = settings.bottomBarSource
|
||||||
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
|
||||||
@ -243,6 +274,8 @@ 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
|
||||||
}
|
}
|
||||||
@ -276,25 +309,56 @@ 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
|
FilledTonalButton(modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(60.dp),
|
.height(60.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
bottomBarDialogVisible = true
|
topBarLeftDialogVisible = true
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
|
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
Text("Bottom Bar: ${bottomSelectedSource.label}", modifier = Modifier.weight(1.0f))
|
Text("Top Bar (Left): ${topSelectedSourceLeft.label}", modifier = Modifier.weight(1.0f))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bottomBarDialogVisible){
|
if (topBarLeftDialogVisible){
|
||||||
BarSelectDialog(bottomSelectedSource, onHide = { bottomBarDialogVisible = false }, onSelect = { selected ->
|
BarSelectDialog(topSelectedSourceLeft, onHide = { topBarLeftDialogVisible = false }, onSelect = { selected ->
|
||||||
bottomSelectedSource = selected
|
topSelectedSourceLeft = selected
|
||||||
coroutineScope.launch { updateSettings() }
|
coroutineScope.launch { updateSettings() }
|
||||||
bottomBarDialogVisible = false
|
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
|
FilledTonalButton(modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(60.dp),
|
.height(60.dp),
|
||||||
@ -305,6 +369,7 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
Spacer(modifier = Modifier.width(5.dp))
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
Text("Top Bar: ${topSelectedSource.label}", modifier = Modifier.weight(1.0f))
|
Text("Top Bar: ${topSelectedSource.label}", modifier = Modifier.weight(1.0f))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (topBarDialogVisible){
|
if (topBarDialogVisible){
|
||||||
BarSelectDialog(topSelectedSource, onHide = { topBarDialogVisible = false }, onSelect = { selected ->
|
BarSelectDialog(topSelectedSource, onHide = { topBarDialogVisible = false }, onSelect = { selected ->
|
||||||
@ -314,6 +379,76 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp),
|
||||||
|
onClick = {
|
||||||
|
bottomBarDialogVisible = true
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
|
||||||
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
|
Text("Bottom Bar: ${bottomSelectedSource.label}", modifier = Modifier.weight(1.0f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottomBarDialogVisible){
|
||||||
|
BarSelectDialog(bottomSelectedSource, onHide = { bottomBarDialogVisible = false }, onSelect = { selected ->
|
||||||
|
bottomSelectedSource = selected
|
||||||
|
coroutineScope.launch { updateSettings() }
|
||||||
|
bottomBarDialogVisible = 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) {
|
||||||
@ -337,14 +472,17 @@ 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 },
|
onValueChange = { minSpeed = it.filter { c -> c.isDigit() } },
|
||||||
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),
|
||||||
@ -355,7 +493,7 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.absolutePadding(left = 2.dp)
|
.absolutePadding(left = 2.dp)
|
||||||
.onFocusEvent(::updateFocus),
|
.onFocusEvent(::updateFocus),
|
||||||
onValueChange = { maxSpeed = it },
|
onValueChange = { maxSpeed = it.filter { c -> c.isDigit() } },
|
||||||
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),
|
||||||
@ -364,7 +502,10 @@ 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
|
||||||
@ -380,7 +521,7 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.absolutePadding(right = 2.dp)
|
.absolutePadding(right = 2.dp)
|
||||||
.onFocusEvent(::updateFocus),
|
.onFocusEvent(::updateFocus),
|
||||||
onValueChange = { customMinPower = it },
|
onValueChange = { customMinPower = it.filter { c -> c.isDigit() } },
|
||||||
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") },
|
||||||
@ -392,7 +533,7 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.absolutePadding(left = 2.dp)
|
.absolutePadding(left = 2.dp)
|
||||||
.onFocusEvent(::updateFocus),
|
.onFocusEvent(::updateFocus),
|
||||||
onValueChange = { customMaxPower = it },
|
onValueChange = { customMaxPower = it.filter { c -> c.isDigit() } },
|
||||||
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") },
|
||||||
@ -403,7 +544,10 @@ 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
|
||||||
@ -419,7 +563,7 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.absolutePadding(right = 2.dp)
|
.absolutePadding(right = 2.dp)
|
||||||
.onFocusEvent(::updateFocus),
|
.onFocusEvent(::updateFocus),
|
||||||
onValueChange = { customMinHr = it },
|
onValueChange = { customMinHr = it.filter { c -> c.isDigit() } },
|
||||||
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 },
|
||||||
@ -431,7 +575,7 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.absolutePadding(left = 2.dp)
|
.absolutePadding(left = 2.dp)
|
||||||
.onFocusEvent(::updateFocus),
|
.onFocusEvent(::updateFocus),
|
||||||
onValueChange = { customMaxHr = it },
|
onValueChange = { customMaxHr = it.filter { c -> c.isDigit() } },
|
||||||
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 },
|
||||||
@ -443,14 +587,17 @@ 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 },
|
onValueChange = { minCadence = it.filter { c -> c.isDigit() } },
|
||||||
label = { Text("Min Cadence") },
|
label = { Text("Min Cadence") },
|
||||||
suffix = { Text("rpm") },
|
suffix = { Text("rpm") },
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
@ -461,7 +608,7 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.absolutePadding(left = 2.dp)
|
.absolutePadding(left = 2.dp)
|
||||||
.onFocusEvent(::updateFocus),
|
.onFocusEvent(::updateFocus),
|
||||||
onValueChange = { maxCadence = it },
|
onValueChange = { maxCadence = it.filter { c -> c.isDigit() } },
|
||||||
label = { Text("Min Cadence") },
|
label = { Text("Min Cadence") },
|
||||||
suffix = { Text("rpm") },
|
suffix = { Text("rpm") },
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
@ -470,6 +617,35 @@ 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
|
||||||
@ -494,7 +670,7 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
coroutineScope.launch { updateSettings() }
|
coroutineScope.launch { updateSettings() }
|
||||||
})
|
})
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text("Opaque background")
|
Text("Solid background")
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|||||||
@ -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.CustomProgressBar
|
<de.timklge.karoopowerbar.CustomView
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/customView"
|
||||||
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" />
|
||||||
|
|||||||
@ -12,4 +12,17 @@
|
|||||||
<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>
|
||||||
BIN
powerbar2.png
BIN
powerbar2.png
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 46 KiB |
BIN
powerbar3.png
Normal file
BIN
powerbar3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Loading…
x
Reference in New Issue
Block a user