Compare commits

..

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

16 changed files with 296 additions and 1109 deletions

View File

@ -45,12 +45,6 @@ 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

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 german localization\n* Add gear data sources\n* 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", "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
@ -42,13 +24,12 @@ class CustomProgressBar(private val view: CustomView,
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
view.invalidate() // Redraw to apply new font size invalidate() // Redraw to apply new font size
} }
var barSize = CustomProgressBarBarSize.MEDIUM var barSize = CustomProgressBarBarSize.MEDIUM
@ -64,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()
@ -139,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!!
@ -149,113 +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
// 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(
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,
@ -277,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,
@ -291,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)
} }
@ -305,44 +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 = (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
@ -365,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()
) )
@ -375,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,
@ -402,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,
@ -416,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)
} }
@ -430,44 +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 = (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.
@ -486,9 +315,4 @@ class CustomProgressBar(private val view: CustomView,
} }
} }
} }
fun invalidate() {
// Invalidate the view to trigger a redraw
view.invalidate()
}
} }

View File

@ -3,17 +3,17 @@ package de.timklge.karoopowerbar
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
enum class CustomProgressBarSize(val id: String, val labelResId: Int, val fontSize: Float, val barHeight: Float) { enum class CustomProgressBarSize(val id: String, val label: String, val fontSize: Float, val barHeight: Float) {
SMALL("small", R.string.size_small, 35f, 10f), SMALL("small", "Small", 35f, 10f),
MEDIUM("medium", R.string.size_medium, 40f, 15f), MEDIUM("medium", "Medium", 40f, 15f),
LARGE("large", R.string.size_large, 60f, 25f), LARGE("large", "Large", 60f, 25f),
} }
@Serializable @Serializable
enum class CustomProgressBarFontSize(val id: String, val labelResId: Int, val fontSize: Float) { enum class CustomProgressBarFontSize(val id: String, val label: String, val fontSize: Float) {
SMALL("small", R.string.size_small, 35f), SMALL("small", "Small", 35f),
MEDIUM("medium", R.string.size_medium, 40f), MEDIUM("medium", "Medium", 40f),
LARGE("large", R.string.size_large, 60f); LARGE("large", "Large", 60f);
companion object { companion object {
fun fromSize(size: CustomProgressBarSize): CustomProgressBarFontSize { fun fromSize(size: CustomProgressBarSize): CustomProgressBarFontSize {
@ -27,11 +27,11 @@ enum class CustomProgressBarFontSize(val id: String, val labelResId: Int, val fo
} }
@Serializable @Serializable
enum class CustomProgressBarBarSize(val id: String, val labelResId: Int, val barHeight: Float) { enum class CustomProgressBarBarSize(val id: String, val label: String, val barHeight: Float) {
NONE("none", R.string.size_none, 0f), NONE("none", "None", 0f),
SMALL("small", R.string.size_small, 10f), SMALL("small", "Small", 10f),
MEDIUM("medium", R.string.size_medium, 15f), MEDIUM("medium", "Medium", 15f),
LARGE("large", R.string.size_large, 25f); LARGE("large", "Large", 25f);
companion object { companion object {
fun fromSize(size: CustomProgressBarSize): CustomProgressBarBarSize { fun fromSize(size: CustomProgressBarSize): CustomProgressBarBarSize {

View File

@ -52,21 +52,19 @@ 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)
}
} }
} }
} }
@ -79,7 +77,7 @@ class ForegroundService : Service() {
private fun setupForeground() { private fun setupForeground() {
val channelId = "de.timklge.karoopowerbar" val channelId = "de.timklge.karoopowerbar"
val channelName = getString(R.string.notification_channel_name) val channelName = "Background Service"
val chan = NotificationChannel( val chan = NotificationChannel(
channelId, channelId,
channelName, channelName,
@ -88,13 +86,14 @@ class ForegroundService : Service() {
val manager = val manager =
checkNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?) checkNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?)
manager.createNotificationChannel(chan) manager.createNotificationChannel(chan)
val notificationBuilder = NotificationCompat.Builder(this, channelId)
val notification = notificationBuilder.setOngoing(true) val notificationBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(this, channelId)
val notification: Notification = notificationBuilder.setOngoing(true)
.setContentTitle("Powerbar service running")
.setContentText("Displaying on top of other apps")
.setSmallIcon(R.drawable.bar) .setSmallIcon(R.drawable.bar)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.notification_text))
.setPriority(NotificationManager.IMPORTANCE_MIN) .setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE) .setCategory(Notification.CATEGORY_SERVICE)
.build() .build()

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,11 +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.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? {
@ -51,15 +50,6 @@ 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,
@ -67,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";
@ -83,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(
@ -97,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()
@ -129,73 +116,45 @@ 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() {
val filter = IntentFilter("de.timklge.HIDE_POWERBAR") serviceJob = CoroutineScope(Dispatchers.Default).launch {
if (Build.VERSION.SDK_INT >= 33) { val filter = IntentFilter("de.timklge.HIDE_POWERBAR")
context.registerReceiver(hideReceiver, filter, Context.RECEIVER_EXPORTED) if (Build.VERSION.SDK_INT >= 33) {
} else { context.registerReceiver(hideReceiver, filter, Context.RECEIVER_EXPORTED)
context.registerReceiver(hideReceiver, filter) } else {
} context.registerReceiver(hideReceiver, filter)
}
karooSystem.connect { connected ->
Log.i(TAG, "Karoo system service connected: $connected") karooSystem.connect { 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() when (selectedSource){
SelectedSource.POWER -> streamPower(PowerStreamSmoothing.RAW)
selectedSources.forEach { selectedSource -> SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S)
serviceJobs.add( CoroutineScope(Dispatchers.IO).launch { SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S)
Log.i(TAG, "Starting stream for $selectedSource") SelectedSource.HEART_RATE -> streamHeartrate()
SelectedSource.SPEED -> streamSpeed(false)
when (selectedSource){ SelectedSource.SPEED_3S -> streamSpeed(true)
SelectedSource.POWER -> streamPower(SelectedSource.POWER, PowerStreamSmoothing.RAW) SelectedSource.CADENCE -> streamCadence(false)
SelectedSource.POWER_3S -> streamPower(SelectedSource.POWER_3S, PowerStreamSmoothing.SMOOTHED_3S) SelectedSource.CADENCE_3S -> streamCadence(true)
SelectedSource.POWER_10S -> streamPower(SelectedSource.POWER_10S, PowerStreamSmoothing.SMOOTHED_10S) SelectedSource.ROUTE_PROGRESS -> streamRouteProgress()
SelectedSource.HEART_RATE -> streamHeartrate() else -> {}
SelectedSource.SPEED -> streamSpeed(SelectedSource.SPEED, false) }
SelectedSource.SPEED_3S -> streamSpeed(SelectedSource.SPEED_3S, true)
SelectedSource.CADENCE -> streamCadence(SelectedSource.CADENCE, false)
SelectedSource.CADENCE_3S -> streamCadence(SelectedSource.CADENCE_3S, true)
SelectedSource.ROUTE_PROGRESS -> streamRouteProgress(SelectedSource.ROUTE_PROGRESS, ::getRouteProgress)
SelectedSource.REMAINING_ROUTE -> streamRouteProgress(SelectedSource.REMAINING_ROUTE, ::getRemainingRouteProgress)
SelectedSource.GRADE -> streamGrade()
SelectedSource.POWER_BALANCE -> streamBalance(PedalBalanceSmoothing.RAW)
SelectedSource.POWER_BALANCE_3S -> streamBalance(PedalBalanceSmoothing.SMOOTHED_3S)
SelectedSource.POWER_BALANCE_10S -> streamBalance(PedalBalanceSmoothing.SMOOTHED_10S)
SelectedSource.FRONT_GEAR -> streamGears(Gears.FRONT)
SelectedSource.REAR_GEAR -> streamGears(Gears.REAR)
SelectedSource.NONE -> {}
}
})
} }
try { try {
@ -209,103 +168,19 @@ class Window(
} }
} }
private suspend fun streamBalance(smoothing: PedalBalanceSmoothing) { private suspend fun streamRouteProgress() {
data class StreamData(val powerBalanceLeft: Double?, val power: Double?)
karooSystem.streamDataFlow(smoothing.dataTypeId)
.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(), 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
@ -327,73 +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 powerbarsWithRouteProgressSource = powerbars.values.filter { it.source == source } val routeProgressInUserUnit = when (userProfile.preferredUnit.distance) {
UserProfile.PreferredUnit.UnitType.IMPERIAL -> routeProgressMeters?.times(0.000621371)?.roundToInt() // Miles
powerbarsWithRouteProgressSource.forEach { powerbar -> else -> routeProgressMeters?.times(0.001)?.roundToInt() // Kilometers
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = barProgress.progress
powerbar.label = barProgress.label ?: ""
powerbar.invalidate()
} }
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = routeProgress
powerbar.label = "$routeProgressInUserUnit"
powerbar.invalidate()
} }
} }
enum class Gears(val prefix: String, val dataTypeId: String, val numberFieldId: String, val maxFieldId: String) { private suspend fun streamSpeed(smoothed: Boolean) {
FRONT("F", DataType.Type.SHIFTING_FRONT_GEAR, DataType.Field.SHIFTING_FRONT_GEAR, DataType.Field.SHIFTING_FRONT_GEAR_MAX),
REAR("R", DataType.Type.SHIFTING_REAR_GEAR, DataType.Field.SHIFTING_REAR_GEAR, DataType.Field.SHIFTING_REAR_GEAR_MAX)
}
private suspend fun streamGears(gears: Gears) {
data class GearsState(val currentGear: Int?, val maxGear: Int?, val colorize: Boolean)
data class StreamState(val settings: PowerbarSettings, val streamState: io.hammerhead.karooext.models.StreamState?)
val gearsSource = when (gears) {
Gears.FRONT -> SelectedSource.FRONT_GEAR
Gears.REAR -> SelectedSource.REAR_GEAR
}
combine(context.streamSettings(), karooSystem.streamDataFlow(gears.dataTypeId)) { settings, streamState -> StreamState(settings, streamState) }
.map { (settings, streamState) ->
val valueMap = (streamState as? io.hammerhead.karooext.models.StreamState.Streaming)?.dataPoint?.values
valueMap?.let {
GearsState(valueMap[gears.numberFieldId]?.toInt(), valueMap[gears.maxFieldId]?.toInt(), settings.useZoneColors)
}
// if (gears == Gears.FRONT) GearsState(1, 2, settings.useZoneColors) else GearsState(6, 12, settings.useZoneColors)
}
.distinctUntilChanged().collect { gearState ->
val powerbarsWithGearsSource = powerbars.values.filter { it.source == gearsSource }
powerbarsWithGearsSource.forEach { powerbar ->
if (gearState?.currentGear != null) {
val currentGear = gearState.currentGear
val maxGear = gearState.maxGear ?: gearState.currentGear
val progress = remap(currentGear.toDouble(), 1.0, maxGear.toDouble(), 0.0, 1.0)
powerbar.progressColor = if (gearState.colorize) {
progress?.let { context.getColor(getZone(progress).colorResource) } ?: context.getColor(R.color.zone0)
} else {
context.getColor(R.color.zone0)
}
powerbar.progress = progress
powerbar.label = "${gears.prefix}${currentGear}"
Log.d(TAG, "Gears ${gears.name}: $currentGear/$maxGear")
} else {
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Gears ${gears.name}: Unavailable")
}
}
}
}
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()
@ -411,95 +235,34 @@ 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 -> val minSpeed = streamData.settings?.minSpeed ?: PowerbarSettings.defaultMinSpeedMs
if (value != null) { val maxSpeed = streamData.settings?.maxSpeed ?: PowerbarSettings.defaultMaxSpeedMs
val minSpeed = streamData.settings?.minSpeed ?: PowerbarSettings.defaultMinSpeedMs val progress = remap(valueMetersPerSecond, minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) ?: 0.0
val maxSpeed = streamData.settings?.maxSpeed ?: PowerbarSettings.defaultMaxSpeedMs
val progress = remap(valueMetersPerSecond, minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) ?: 0.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
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
context.getColor(zoneColorRes) context.getColor(zoneColorRes)
} else {
context.getColor(R.color.zone0)
}
powerbar.progress = progress
powerbar.label = "$value"
Log.d(TAG, "Speed: $value min: $minSpeed max: $maxSpeed")
} else { } else {
powerbar.progressColor = context.getColor(R.color.zone0) context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Speed: Unavailable")
} }
powerbar.invalidate() powerbar.progress = if (value > 0) progress else null
} powerbar.label = "$value"
}
}
private suspend fun streamGrade() { Log.d(TAG, "Speed: $value min: $minSpeed max: $maxSpeed")
@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 { } else {
powerbar.progressColor = context.getColor(R.color.zone0) powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = null powerbar.progress = null
powerbar.label = "?" powerbar.label = "?"
Log.d(TAG, "Grade: Unavailable") Log.d(TAG, "Speed: Unavailable")
} }
powerbar.invalidate() powerbar.invalidate()
} }
}
} }
private suspend fun streamCadence(source: SelectedSource, smoothed: Boolean) { private suspend fun streamCadence(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()
@ -515,38 +278,35 @@ 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
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
context.getColor(zoneColorRes) context.getColor(zoneColorRes)
} else {
context.getColor(R.color.zone0)
}
powerbar.progress = progress
powerbar.label = "$value"
Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence")
} else { } else {
powerbar.progressColor = context.getColor(R.color.zone0) context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Cadence: Unavailable")
} }
powerbar.invalidate() powerbar.progress = if (value > 0) progress else null
powerbar.label = "$value"
Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence")
} else {
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Cadence: Unavailable")
} }
powerbar.invalidate()
} }
} }
@ -566,38 +326,35 @@ 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 val minHr = customMinHr ?: streamData.userProfile.restingHr
val minHr = customMinHr ?: streamData.userProfile.restingHr 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)
} else {
context.getColor(R.color.zone0)
}
powerbar.progress = progress
powerbar.label = "$value"
Log.d(TAG, "Hr: $value min: $minHr max: $maxHr")
} else { } else {
powerbar.progressColor = context.getColor(R.color.zone0) context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Hr: Unavailable")
} }
powerbar.invalidate() powerbar.progress = if (value > 0) progress else null
powerbar.label = "$value"
Log.d(TAG, "Hr: $value min: $minHr max: $maxHr")
} else {
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Hr: Unavailable")
} }
powerbar.invalidate()
} }
} }
@ -607,13 +364,7 @@ class Window(
SMOOTHED_10S(DataType.Type.SMOOTHED_10S_AVERAGE_POWER), SMOOTHED_10S(DataType.Type.SMOOTHED_10S_AVERAGE_POWER),
} }
enum class PedalBalanceSmoothing(val dataTypeId: String){ private suspend fun streamPower(smoothed: PowerStreamSmoothing) {
RAW(DataType.Type.PEDAL_POWER_BALANCE),
SMOOTHED_3S(DataType.Type.SMOOTHED_3S_AVERAGE_PEDAL_POWER_BALANCE),
SMOOTHED_10S(DataType.Type.SMOOTHED_10S_AVERAGE_PEDAL_POWER_BALANCE),
}
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()
@ -630,38 +381,35 @@ 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 val minPower = customMinPower ?: streamData.userProfile.powerZones.first().min
val minPower = customMinPower ?: streamData.userProfile.powerZones.first().min 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)
} else {
context.getColor(R.color.zone0)
}
powerbar.progress = progress
powerbar.label = "${value}W"
Log.d(TAG, "Power: $value min: $minPower max: $maxPower")
} else { } else {
powerbar.progressColor = context.getColor(R.color.zone0) context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Power: Unavailable")
} }
powerbar.invalidate() powerbar.progress = if (value > 0) progress else null
powerbar.label = "${value}W"
Log.d(TAG, "Power: $value min: $minPower max: $maxPower")
} else {
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = null
powerbar.label = "?"
Log.d(TAG, "Power: Unavailable")
} }
powerbar.invalidate()
} }
} }
@ -674,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

@ -37,10 +37,3 @@ fun getZone(userZones: List<UserProfile.Zone>, value: Int): Zone? {
return null return null
} }
val zoneList = listOf(Zone.Zone0, Zone.Zone1, Zone.Zone2, Zone.Zone3, Zone.Zone4, Zone.Zone5, Zone.Zone6, Zone.Zone7, Zone.Zone8)
fun getZone(progress: Double): Zone {
val index = (progress * zoneList.size).toInt().coerceIn(0, zoneList.size - 1)
return zoneList[index]
}

View File

@ -50,7 +50,6 @@ import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@ -60,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
@ -78,24 +78,17 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlin.math.roundToInt import kotlin.math.roundToInt
enum class SelectedSource(val id: String, val labelResId: Int) { enum class SelectedSource(val id: String, val label: String) {
NONE("none", R.string.source_none), NONE("none", "None"),
HEART_RATE("hr", R.string.source_heart_rate), HEART_RATE("hr", "Heart Rate"),
POWER("power", R.string.source_power), POWER("power", "Power"),
POWER_3S("power_3s", R.string.source_power_3s), POWER_3S("power_3s", "Power (3 sec avg)"),
POWER_10S("power_10s", R.string.source_power_10s), POWER_10S("power_10s", "Power (10 sec avg)"),
SPEED("speed", R.string.source_speed), SPEED("speed", "Speed"),
SPEED_3S("speed_3s", R.string.source_speed_3s), SPEED_3S("speed_3s", "Speed (3 sec avg"),
CADENCE("cadence", R.string.source_cadence), CADENCE("cadence", "Cadence"),
CADENCE_3S("cadence_3s", R.string.source_cadence_3s), CADENCE_3S("cadence_3s", "Cadence (3 sec avg)"),
GRADE("grade", R.string.source_grade), ROUTE_PROGRESS("route_progress", "Route Progress");
POWER_BALANCE("power_balance", R.string.source_power_balance),
POWER_BALANCE_3S("power_balance_3s", R.string.source_power_balance_3s),
POWER_BALANCE_10S("power_balance_10s", R.string.source_power_balance_10s),
ROUTE_PROGRESS("route_progress", R.string.source_route_progress),
REMAINING_ROUTE("route_progress_remaining", R.string.source_route_remaining),
FRONT_GEAR("front_gear", R.string.source_front_gear),
REAR_GEAR("rear_gear", R.string.source_rear_gear);
fun isPower() = this == POWER || this == POWER_3S || this == POWER_10S fun isPower() = this == POWER || this == POWER_3S || this == POWER_10S
} }
@ -124,7 +117,7 @@ fun BarSelectDialog(currentSelectedSource: SelectedSource, onHide: () -> Unit, o
onSelect(pattern) onSelect(pattern)
}) })
Text( Text(
text = stringResource(pattern.labelResId), text = pattern.label,
modifier = Modifier.padding(start = 10.dp) modifier = Modifier.padding(start = 10.dp)
) )
} }
@ -145,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) }
@ -180,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) }
@ -199,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,
@ -216,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,
@ -256,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
@ -279,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,163 +270,21 @@ fun MainScreen(onFinish: () -> Unit) {
Column(modifier = Modifier Column(modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background)) { .background(MaterialTheme.colorScheme.background)) {
TopAppBar(title = { Text(stringResource(R.string.powerbar_title)) }) TopAppBar(title = { Text("Powerbar") })
Column(modifier = Modifier Column(modifier = Modifier
.padding(5.dp) .padding(5.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) { .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
if (showAlerts){ FilledTonalButton(modifier = Modifier
if(!karooConnected){ .fillMaxWidth()
Text(modifier = Modifier.padding(5.dp), text = stringResource(R.string.karoo_connection_error)) .height(60.dp),
} onClick = {
bottomBarDialogVisible = true
if (!givenPermissions) { }) {
Text(modifier = Modifier.padding(5.dp), text = stringResource(R.string.permission_not_granted)) Icon(Icons.Default.Build, contentDescription = "Select", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
FilledTonalButton(modifier = Modifier Text("Bottom Bar: ${bottomSelectedSource.label}", modifier = Modifier.weight(1.0f))
.fillMaxWidth()
.height(50.dp), onClick = {
val myIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(ctx, myIntent, null)
}) {
Icon(Icons.Default.Build, contentDescription = stringResource(R.string.content_desc_give_permission))
Spacer(modifier = Modifier.width(5.dp))
Text(stringResource(R.string.give_permission))
}
}
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp)) {
Text(stringResource(R.string.top_bar), style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.weight(1f))
Text(stringResource(R.string.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 = stringResource(R.string.content_desc_select), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text(stringResource(R.string.top_bar_left, stringResource(topSelectedSourceLeft.labelResId)), 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 = stringResource(R.string.content_desc_select), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text(stringResource(R.string.top_bar_right, stringResource(topSelectedSourceRight.labelResId)), 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 = stringResource(R.string.content_desc_select), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text(stringResource(R.string.top_bar_single, stringResource(topSelectedSource.labelResId)), 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(stringResource(R.string.bottom_bar), style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.weight(1f))
Text(stringResource(R.string.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 = stringResource(R.string.content_desc_select), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text(stringResource(R.string.bottom_bar_left, stringResource(bottomSelectedSourceLeft.labelResId)), 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 = stringResource(R.string.content_desc_select), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text(stringResource(R.string.bottom_bar_right, stringResource(bottomSelectedSourceRight.labelResId)), 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 = stringResource(R.string.content_desc_select), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text(stringResource(R.string.bottom_bar_single, stringResource(bottomSelectedSource.labelResId)), modifier = Modifier.weight(1.0f))
}
} }
if (bottomBarDialogVisible){ if (bottomBarDialogVisible){
@ -475,42 +295,58 @@ 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, stringResource(unit.labelResId)) } val dropdownOptions = CustomProgressBarBarSize.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val dropdownInitialSelection by remember(barBarSize) { val dropdownInitialSelection by remember(barBarSize) {
mutableStateOf(dropdownOptions.find { option -> option.id == barBarSize.id }!!) mutableStateOf(dropdownOptions.find { option -> option.id == barBarSize.id }!!)
} }
Dropdown(label = stringResource(R.string.bar_size), options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption -> Dropdown(label = "Bar Size", options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption ->
barBarSize = CustomProgressBarBarSize.entries.find { unit -> unit.id == selectedOption.id }!! barBarSize = CustomProgressBarBarSize.entries.find { unit -> unit.id == selectedOption.id }!!
coroutineScope.launch { updateSettings() } coroutineScope.launch { updateSettings() }
} }
} }
apply { apply {
val dropdownOptions = CustomProgressBarFontSize.entries.toList().map { unit -> DropdownOption(unit.id, stringResource(unit.labelResId)) } val dropdownOptions = CustomProgressBarFontSize.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val dropdownInitialSelection by remember(barFontSize) { val dropdownInitialSelection by remember(barFontSize) {
mutableStateOf(dropdownOptions.find { option -> option.id == barFontSize.id }!!) mutableStateOf(dropdownOptions.find { option -> option.id == barFontSize.id }!!)
} }
Dropdown(label = stringResource(R.string.text_size), options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption -> Dropdown(label = "Text Size", options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption ->
barFontSize = CustomProgressBarFontSize.entries.find { unit -> unit.id == selectedOption.id }!! barFontSize = CustomProgressBarFontSize.entries.find { unit -> unit.id == selectedOption.id }!!
coroutineScope.launch { updateSettings() } coroutineScope.launch { updateSettings() }
} }
} }
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(stringResource(R.string.min_speed)) }, label = { Text("Min Speed") },
suffix = { Text(stringResource(if (isImperial) R.string.unit_mph else R.string.unit_kph)) }, suffix = { Text(if (isImperial) "mph" else "kph") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
) )
@ -519,26 +355,23 @@ 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(stringResource(R.string.max_speed)) }, label = { Text("Max Speed") },
suffix = { Text(stringResource(if (isImperial) R.string.unit_mph else R.string.unit_kph)) }, suffix = { Text(if (isImperial) "mph" else "kph") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
) )
} }
} }
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
coroutineScope.launch { updateSettings() } coroutineScope.launch { updateSettings() }
}) })
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text(stringResource(R.string.use_custom_power_range)) Text("Use custom power range")
} }
if(useCustomPowerRange){ if(useCustomPowerRange){
@ -547,9 +380,9 @@ 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(stringResource(R.string.min_power), fontSize = 12.sp) }, label = { Text("Min Power", fontSize = 12.sp) },
suffix = { Text(stringResource(R.string.unit_watts)) }, suffix = { Text("W") },
placeholder = { Text("$profileMinPower") }, placeholder = { Text("$profileMinPower") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
@ -559,9 +392,9 @@ 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(stringResource(R.string.max_power), fontSize = 12.sp) }, label = { Text("Max Power", fontSize = 12.sp) },
suffix = { Text(stringResource(R.string.unit_watts)) }, suffix = { Text("W") },
placeholder = { Text("$profileMaxPower") }, placeholder = { Text("$profileMaxPower") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
@ -570,17 +403,14 @@ 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
coroutineScope.launch { updateSettings() } coroutineScope.launch { updateSettings() }
}) })
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text(stringResource(R.string.use_custom_hr_range)) Text("Use custom HR range")
} }
if (useCustomHrRange){ if (useCustomHrRange){
@ -589,9 +419,9 @@ 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(stringResource(R.string.min_hr)) }, label = { Text("Min Hr") },
suffix = { Text(stringResource(R.string.unit_bpm)) }, suffix = { Text("bpm") },
placeholder = { if(profileRestHr > 0) Text("$profileRestHr") else Unit }, placeholder = { if(profileRestHr > 0) Text("$profileRestHr") else Unit },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
@ -601,9 +431,9 @@ 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(stringResource(R.string.max_hr)) }, label = { Text("Max Hr") },
suffix = { Text(stringResource(R.string.unit_bpm)) }, suffix = { Text("bpm") },
placeholder = { if(profileMaxHr > 0) Text("$profileMaxHr") else Unit }, placeholder = { if(profileMaxHr > 0) Text("$profileMaxHr") else Unit },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
@ -613,19 +443,16 @@ 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(stringResource(R.string.min_cadence)) }, label = { Text("Min Cadence") },
suffix = { Text(stringResource(R.string.unit_rpm)) }, suffix = { Text("rpm") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
) )
@ -634,38 +461,9 @@ 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(stringResource(R.string.max_cadence)) }, label = { Text("Min Cadence") },
suffix = { Text(stringResource(R.string.unit_rpm)) }, suffix = { Text("rpm") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true
)
}
}
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(stringResource(R.string.min_grade)) },
suffix = { Text(stringResource(R.string.unit_percent)) },
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(stringResource(R.string.max_grade)) },
suffix = { Text(stringResource(R.string.unit_percent)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
) )
@ -678,7 +476,7 @@ fun MainScreen(onFinish: () -> Unit) {
coroutineScope.launch { updateSettings() } coroutineScope.launch { updateSettings() }
}) })
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text(stringResource(R.string.color_based_on_zones)) Text("Color based on HR / power zones")
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@ -687,7 +485,7 @@ fun MainScreen(onFinish: () -> Unit) {
coroutineScope.launch { updateSettings() } coroutineScope.launch { updateSettings() }
}) })
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text(stringResource(R.string.show_value_on_bars)) Text("Show value on bars")
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@ -696,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(stringResource(R.string.solid_background)) Text("Opaque background")
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@ -705,16 +503,37 @@ fun MainScreen(onFinish: () -> Unit) {
coroutineScope.launch { updateSettings() } coroutineScope.launch { updateSettings() }
}) })
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text(stringResource(R.string.only_show_while_riding)) Text("Only show while riding")
} }
Spacer(modifier = Modifier.padding(30.dp)) Spacer(modifier = Modifier.padding(30.dp))
if (showAlerts){
if(!karooConnected){
Text(modifier = Modifier.padding(5.dp), text = "Could not read device status. Is your Karoo updated?")
}
if (!givenPermissions) {
Text(modifier = Modifier.padding(5.dp), text = "You have not given permissions to show the power bar overlay. Please do so.")
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(50.dp), onClick = {
val myIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(ctx, myIntent, null)
}) {
Icon(Icons.Default.Build, contentDescription = "Give permission")
Spacer(modifier = Modifier.width(5.dp))
Text("Give permission")
}
}
}
} }
} }
Image( Image(
painter = painterResource(id = R.drawable.back), painter = painterResource(id = R.drawable.back),
contentDescription = stringResource(R.string.content_desc_back), contentDescription = "Back",
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.padding(bottom = 10.dp) .padding(bottom = 10.dp)

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

@ -1,79 +0,0 @@
<resources>
<string name="app_name">Powerbar</string>
<string name="extension_name">Powerbar</string>
<!-- Main Screen Strings -->
<string name="powerbar_title">Powerbar</string>
<string name="karoo_connection_error">Gerätestatus konnte nicht gelesen werden. Ist Ihr Karoo aktualisiert?</string>
<string name="permission_not_granted">Sie haben die Berechtigung zum Anzeigen der Powerbar nicht erteilt. Bitte tun Sie dies.</string>
<string name="give_permission">Berechtigung erteilen</string>
<string name="top_bar">Oben</string>
<string name="bottom_bar">Unten</string>
<string name="split">Teilen</string>
<string name="top_bar_left">Oben (Links): %s</string>
<string name="top_bar_right">Oben (Rechts): %s</string>
<string name="top_bar_single">Oben: %s</string>
<string name="bottom_bar_left">Unten (Links): %s</string>
<string name="bottom_bar_right">Unten (Rechts): %s</string>
<string name="bottom_bar_single">Unten: %s</string>
<string name="bar_size">Balkengröße</string>
<string name="text_size">Textgröße</string>
<string name="min_speed">Min. Geschwindigkeit</string>
<string name="max_speed">Max. Geschwindigkeit</string>
<string name="use_custom_power_range">Eigenen Leistungsbereich verwenden</string>
<string name="min_power">Min. Leistung</string>
<string name="max_power">Max. Leistung</string>
<string name="use_custom_hr_range">Eigenen HF-Bereich verwenden</string>
<string name="min_hr">Min. HF</string>
<string name="max_hr">Max. HF</string>
<string name="min_cadence">Min. Trittfrequenz</string>
<string name="max_cadence">Max. Trittfrequenz</string>
<string name="min_grade">Min. Steigung</string>
<string name="max_grade">Max. Steigung</string>
<string name="color_based_on_zones">Farbe basierend auf HF-/Leistungszonen</string>
<string name="show_value_on_bars">Werte auf Balken anzeigen</string>
<string name="solid_background">Fester Hintergrund</string>
<string name="only_show_while_riding">Nur während der Fahrt anzeigen</string>
<!-- Data Source Labels -->
<string name="source_none">Keine</string>
<string name="source_heart_rate">Herzfrequenz</string>
<string name="source_power">Leistung</string>
<string name="source_power_3s">Leistung (3 Sek. Ø)</string>
<string name="source_power_10s">Leistung (10 Sek. Ø)</string>
<string name="source_speed">Geschwindigkeit</string>
<string name="source_speed_3s">Geschwindigkeit (3 Sek. Ø)</string>
<string name="source_cadence">Trittfrequenz</string>
<string name="source_cadence_3s">Trittfrequenz (3 Sek. Ø)</string>
<string name="source_grade">Steigung</string>
<string name="source_power_balance">Leistungsbalance</string>
<string name="source_power_balance_3s">Leistungsbalance (3 Sek. Ø)</string>
<string name="source_power_balance_10s">Leistungsbalance (10 Sek. Ø)</string>
<string name="source_route_progress">Routenfortschritt</string>
<string name="source_route_remaining">Verbleibende Route</string>
<string name="source_front_gear">Vorderer Gang</string>
<string name="source_rear_gear">Hinterer Gang</string>
<!-- Units -->
<string name="unit_mph">mph</string>
<string name="unit_kph">km/h</string>
<string name="unit_watts">W</string>
<string name="unit_bpm">S/min</string>
<string name="unit_rpm">U/min</string>
<string name="unit_percent">%</string>
<!-- Content Descriptions -->
<string name="content_desc_give_permission">Berechtigung erteilen</string>
<string name="content_desc_select">Auswählen</string>
<string name="content_desc_back">Zurück</string>
<!-- Notification -->
<string name="notification_text">Anzeige über anderen Apps</string>
<string name="notification_channel_name">Hintergrunddienst</string>
<!-- Size Options -->
<string name="size_none">Keine</string>
<string name="size_small">Klein</string>
<string name="size_medium">Mittel</string>
<string name="size_large">Groß</string>
</resources>

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>

View File

@ -1,79 +1,4 @@
<resources> <resources>
<string name="app_name">Powerbar</string> <string name="app_name">Powerbar</string>
<string name="extension_name">Powerbar</string> <string name="extension_name">Powerbar</string>
<!-- Main Screen Strings -->
<string name="powerbar_title">Powerbar</string>
<string name="karoo_connection_error">Could not read device status. Is your Karoo updated?</string>
<string name="permission_not_granted">You have not granted the permission to show the power bar overlay. Please do so.</string>
<string name="give_permission">Give permission</string>
<string name="top_bar">Top Bar</string>
<string name="bottom_bar">Bottom Bar</string>
<string name="split">Split</string>
<string name="top_bar_left">Top Bar (Left): %s</string>
<string name="top_bar_right">Top Bar (Right): %s</string>
<string name="top_bar_single">Top Bar: %s</string>
<string name="bottom_bar_left">Bottom Bar (Left): %s</string>
<string name="bottom_bar_right">Bottom Bar (Right): %s</string>
<string name="bottom_bar_single">Bottom Bar: %s</string>
<string name="bar_size">Bar Size</string>
<string name="text_size">Text Size</string>
<string name="min_speed">Min Speed</string>
<string name="max_speed">Max Speed</string>
<string name="use_custom_power_range">Use custom power range</string>
<string name="min_power">Min Power</string>
<string name="max_power">Max Power</string>
<string name="use_custom_hr_range">Use custom HR range</string>
<string name="min_hr">Min Hr</string>
<string name="max_hr">Max Hr</string>
<string name="min_cadence">Min Cadence</string>
<string name="max_cadence">Max Cadence</string>
<string name="min_grade">Min Grade</string>
<string name="max_grade">Max Grade</string>
<string name="color_based_on_zones">Color based on HR / power zones</string>
<string name="show_value_on_bars">Show value on bars</string>
<string name="solid_background">Solid background</string>
<string name="only_show_while_riding">Only show while riding</string>
<!-- Data Source Labels -->
<string name="source_none">None</string>
<string name="source_heart_rate">Heart Rate</string>
<string name="source_power">Power</string>
<string name="source_power_3s">Power (3 sec avg)</string>
<string name="source_power_10s">Power (10 sec avg)</string>
<string name="source_speed">Speed</string>
<string name="source_speed_3s">Speed (3 sec avg)</string>
<string name="source_cadence">Cadence</string>
<string name="source_cadence_3s">Cadence (3 sec avg)</string>
<string name="source_grade">Grade</string>
<string name="source_power_balance">Power Balance</string>
<string name="source_power_balance_3s">Power Balance (3 sec avg)</string>
<string name="source_power_balance_10s">Power Balance (10 sec avg)</string>
<string name="source_route_progress">Route Progress</string>
<string name="source_route_remaining">Route Remaining</string>
<string name="source_front_gear">Front Gear</string>
<string name="source_rear_gear">Rear Gear</string>
<!-- Units -->
<string name="unit_mph">mph</string>
<string name="unit_kph">kph</string>
<string name="unit_watts">W</string>
<string name="unit_bpm">bpm</string>
<string name="unit_rpm">rpm</string>
<string name="unit_percent">%</string>
<!-- Content Descriptions -->
<string name="content_desc_give_permission">Give permission</string>
<string name="content_desc_select">Select</string>
<string name="content_desc_back">Back</string>
<!-- Notification -->
<string name="notification_text">Displaying on top of other apps</string>
<string name="notification_channel_name">Background Service</string>
<!-- Size Options -->
<string name="size_none">None</string>
<string name="size_small">Small</string>
<string name="size_medium">Medium</string>
<string name="size_large">Large</string>
</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