Add weather view to main menu (#54)

* Add initial weather widget to main menu

* Always forecast position with calculated distance

* Fix manifest

* Replace exit button with back button
This commit is contained in:
timklge 2025-03-02 15:18:07 +01:00 committed by GitHub
parent de009454b8
commit ee206a11c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 815 additions and 374 deletions

View File

@ -8,6 +8,8 @@ This extension for Karoo devices adds a graphical data field that shows the curr
Compatible with Karoo 2 and Karoo 3 devices. Compatible with Karoo 2 and Karoo 3 devices.
<a href="https://www.buymeacoffee.com/timklge" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
![Settings](preview0.png) ![Settings](preview0.png)
![Field setup](preview1.png) ![Field setup](preview1.png)
![Data page](preview2.png) ![Data page](preview2.png)

View File

@ -7,5 +7,5 @@
"latestVersionCode": 14, "latestVersionCode": 14,
"developer": "timklge", "developer": "timklge",
"description": "Provides headwind direction, wind speed and other weather data fields", "description": "Provides headwind direction, wind speed and other weather data fields",
"releaseNotes": "Forecast weather along route in fixed intervals if route is loaded", "releaseNotes": "* Forecast weather along route in fixed intervals if route is loaded\n* Show current weather in app menu"
} }

View File

@ -8,10 +8,6 @@ import com.mapbox.geojson.LineString
import com.mapbox.turf.TurfConstants import com.mapbox.turf.TurfConstants
import com.mapbox.turf.TurfMeasurement import com.mapbox.turf.TurfMeasurement
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindStats
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
import de.timklge.karooheadwind.screens.WindUnit
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.DataType import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.OnNavigationState import io.hammerhead.karooext.models.OnNavigationState
@ -21,16 +17,13 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -102,7 +95,8 @@ fun Context.streamSettings(karooSystemService: KarooSystemService): Flow<Headwin
if (settingsJson.contains(settingsKey)){ if (settingsJson.contains(settingsKey)){
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(settingsJson[settingsKey]!!) jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(settingsJson[settingsKey]!!)
} else { } else {
val defaultSettings = jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings) val defaultSettings = jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(
HeadwindSettings.defaultSettings)
val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit
@ -138,6 +132,7 @@ fun KarooSystemService.streamUpcomingRoute(): Flow<UpcomingRoute?> {
navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) } navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) }
} }
.combine(distanceToDestinationStream) { routePolyline, distanceToDestination -> .combine(distanceToDestinationStream) { routePolyline, distanceToDestination ->
Log.d(KarooHeadwindExtension.TAG, "Route polyline size: ${routePolyline?.coordinates()?.size}, distance to destination: $distanceToDestination")
if (routePolyline != null){ if (routePolyline != null){
val length = TurfMeasurement.length(routePolyline, TurfConstants.UNIT_METERS) val length = TurfMeasurement.length(routePolyline, TurfConstants.UNIT_METERS)
if (routePolyline != lastKnownRoutePolyline){ if (routePolyline != lastKnownRoutePolyline){

View File

@ -1,67 +1,15 @@
package de.timklge.karooheadwind package de.timklge.karooheadwind
import android.content.Context
import android.util.Log
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.glance.GlanceModifier
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews
import androidx.glance.appwidget.RemoteViewsCompositionResult
import androidx.glance.color.ColorProvider
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindStats
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
import de.timklge.karooheadwind.screens.PrecipitationUnit
import de.timklge.karooheadwind.screens.TemperatureUnit
import de.timklge.karooheadwind.screens.WindUnit
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.HttpResponseState
import io.hammerhead.karooext.models.OnHttpResponse
import io.hammerhead.karooext.models.OnLocationChanged import io.hammerhead.karooext.models.OnLocationChanged
import io.hammerhead.karooext.models.OnNavigationState import io.hammerhead.karooext.models.OnNavigationState
import io.hammerhead.karooext.models.OnStreamState import io.hammerhead.karooext.models.OnStreamState
import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.time.debounce
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Duration
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.seconds
fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> { fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
return callbackFlow { return callbackFlow {

View File

@ -91,7 +91,10 @@ suspend fun KarooSystemService.updateLastKnownGps(context: Context) {
} }
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> { fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> {
// return flowOf(GpsCoordinates(52.5164069,13.3784)) /* return flow {
emit(GpsCoordinates(52.5164069,13.3784))
awaitCancellation()
} */
val initialFlow = flow { val initialFlow = flow {
val lastKnownPosition = context.getLastKnownPosition() val lastKnownPosition = context.getLastKnownPosition()

View File

@ -0,0 +1,78 @@
package de.timklge.karooheadwind
import de.timklge.karooheadwind.datatypes.GpsCoordinates
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
enum class WindUnit(val id: String, val label: String, val unitDisplay: String){
KILOMETERS_PER_HOUR("kmh", "Kilometers (km/h)", "km/h"),
METERS_PER_SECOND("ms", "Meters (m/s)", "m/s"),
MILES_PER_HOUR("mph", "Miles (mph)", "mph"),
KNOTS("kn", "Knots (kn)", "kn")
}
enum class PrecipitationUnit(val id: String, val label: String, val unitDisplay: String){
MILLIMETERS("mm", "Millimeters (mm)", "mm"),
INCH("inch", "Inch", "in")
}
enum class WindDirectionIndicatorTextSetting(val id: String, val label: String){
HEADWIND_SPEED("headwind-speed", "Headwind speed"),
WIND_SPEED("absolute-wind-speed", "Absolute wind speed"),
NONE("none", "None")
}
enum class WindDirectionIndicatorSetting(val id: String, val label: String){
HEADWIND_DIRECTION("headwind-direction", "Headwind"),
WIND_DIRECTION("wind-direction", "Absolute wind direction"),
}
enum class TemperatureUnit(val id: String, val label: String, val unitDisplay: String){
CELSIUS("celsius", "Celsius (°C)", "°C"),
FAHRENHEIT("fahrenheit", "Fahrenheit (°F)", "°F")
}
enum class RoundLocationSetting(val id: String, val label: String, val km: Int){
KM_1("1 km", "1 km", 1),
KM_2("2 km", "2 km", 2),
KM_5("5 km", "5 km", 5)
}
@Serializable
data class HeadwindWidgetSettings(
val currentForecastHourOffset: Int = 0
){
companion object {
val defaultWidgetSettings = Json.encodeToString(HeadwindWidgetSettings())
}
}
@Serializable
data class HeadwindStats(
val lastSuccessfulWeatherRequest: Long? = null,
val lastSuccessfulWeatherPosition: GpsCoordinates? = null,
val failedWeatherRequest: Long? = null,
){
companion object {
val defaultStats = Json.encodeToString(HeadwindStats())
}
}
@Serializable
data class HeadwindSettings(
val windUnit: WindUnit = WindUnit.KILOMETERS_PER_HOUR,
val welcomeDialogAccepted: Boolean = false,
val windDirectionIndicatorTextSetting: WindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.HEADWIND_SPEED,
val windDirectionIndicatorSetting: WindDirectionIndicatorSetting = WindDirectionIndicatorSetting.HEADWIND_DIRECTION,
val roundLocationTo: RoundLocationSetting = RoundLocationSetting.KM_2,
val forecastedKmPerHour: Int = 20,
val forecastedMilesPerHour: Int = 12,
){
companion object {
val defaultSettings = Json.encodeToString(HeadwindSettings())
}
fun getForecastMetersPerHour(isImperial: Boolean): Int {
return if (isImperial) forecastedMilesPerHour * 1609 else forecastedKmPerHour * 1000
}
}

View File

@ -7,13 +7,13 @@ import com.mapbox.turf.TurfConstants
import com.mapbox.turf.TurfMeasurement import com.mapbox.turf.TurfMeasurement
import de.timklge.karooheadwind.datatypes.CloudCoverDataType import de.timklge.karooheadwind.datatypes.CloudCoverDataType
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
import de.timklge.karooheadwind.datatypes.PrecipitationDataType import de.timklge.karooheadwind.datatypes.PrecipitationDataType
import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
import de.timklge.karooheadwind.datatypes.TemperatureDataType import de.timklge.karooheadwind.datatypes.TemperatureDataType
import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType
import de.timklge.karooheadwind.datatypes.WeatherDataType import de.timklge.karooheadwind.datatypes.WeatherDataType
@ -21,9 +21,6 @@ import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
import de.timklge.karooheadwind.datatypes.WindDirectionDataType import de.timklge.karooheadwind.datatypes.WindDirectionDataType
import de.timklge.karooheadwind.datatypes.WindGustsDataType import de.timklge.karooheadwind.datatypes.WindGustsDataType
import de.timklge.karooheadwind.datatypes.WindSpeedDataType import de.timklge.karooheadwind.datatypes.WindSpeedDataType
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindStats
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.KarooExtension import io.hammerhead.karooext.extension.KarooExtension
import io.hammerhead.karooext.models.UserProfile import io.hammerhead.karooext.models.UserProfile
@ -47,7 +44,6 @@ import kotlinx.coroutines.withContext
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.Locale
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -159,27 +155,46 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
ChronoUnit.MILLIS.between(startOfHour, now) ChronoUnit.MILLIS.between(startOfHour, now)
} }
val msToNextFullHour = (1_000 * 60 * 60) - msSinceFullHour val msToNextFullHour = (1_000 * 60 * 60) - msSinceFullHour
val calculatedDistanceToNextFullHour = (msToNextFullHour / (1_000.0 * 60 * 60)) * distancePerHour val calculatedDistanceToNextFullHour = ((msToNextFullHour / (1_000.0 * 60 * 60)) * distancePerHour).coerceIn(0.0, distancePerHour)
val distanceToNextFullHour = if (calculatedDistanceToNextFullHour > 5_000) calculatedDistanceToNextFullHour else distancePerHour
Log.d(TAG, "Minutes to next full hour: ${msToNextFullHour / 1000 / 60}, Distance to next full hour: ${(distanceToNextFullHour / 1000).roundToInt()}km (calculated: ${(calculatedDistanceToNextFullHour / 1000).roundToInt()}km)") Log.d(TAG, "Minutes to next full hour: ${msToNextFullHour / 1000 / 60}, Distance to next full hour: ${(calculatedDistanceToNextFullHour / 1000).roundToInt()}km")
requestedGpsCoordinates = buildList { requestedGpsCoordinates = buildList {
add(gps) add(gps)
var currentPosition = positionOnRoute + distanceToNextFullHour var currentPosition = positionOnRoute + calculatedDistanceToNextFullHour
var lastRequestedPosition = currentPosition var lastRequestedPosition = currentPosition
while (currentPosition < upcomingRoute.routeLength && size < 10){ while (currentPosition < upcomingRoute.routeLength && size < 10) {
val point = TurfMeasurement.along(upcomingRoute.routePolyline, currentPosition, TurfConstants.UNIT_METERS) val point = TurfMeasurement.along(
add(GpsCoordinates(point.latitude(), point.longitude(), distanceAlongRoute = currentPosition)) upcomingRoute.routePolyline,
currentPosition,
TurfConstants.UNIT_METERS
)
add(
GpsCoordinates(
point.latitude(),
point.longitude(),
distanceAlongRoute = currentPosition
)
)
lastRequestedPosition = currentPosition lastRequestedPosition = currentPosition
currentPosition += distancePerHour currentPosition += distancePerHour
} }
if (upcomingRoute.routeLength > lastRequestedPosition + 5_000){ if (upcomingRoute.routeLength > lastRequestedPosition + 5_000) {
val point = TurfMeasurement.along(upcomingRoute.routePolyline, upcomingRoute.routeLength, TurfConstants.UNIT_METERS) val point = TurfMeasurement.along(
add(GpsCoordinates(point.latitude(), point.longitude(), distanceAlongRoute = upcomingRoute.routeLength)) upcomingRoute.routePolyline,
upcomingRoute.routeLength,
TurfConstants.UNIT_METERS
)
add(
GpsCoordinates(
point.latitude(),
point.longitude(),
distanceAlongRoute = upcomingRoute.routeLength
)
)
} }
} }
} else { } else {

View File

@ -2,9 +2,6 @@ package de.timklge.karooheadwind
import android.util.Log import android.util.Log
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.PrecipitationUnit
import de.timklge.karooheadwind.screens.TemperatureUnit
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.HttpResponseState import io.hammerhead.karooext.models.HttpResponseState
import io.hammerhead.karooext.models.OnHttpResponse import io.hammerhead.karooext.models.OnHttpResponse

View File

@ -7,12 +7,11 @@ import androidx.compose.ui.unit.DpSize
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews import androidx.glance.appwidget.GlanceRemoteViews
import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
import de.timklge.karooheadwind.getRelativeHeadingFlow import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.WindDirectionIndicatorSetting
import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamDataFlow
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl import io.hammerhead.karooext.extension.DataTypeImpl
@ -30,8 +29,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt

View File

@ -43,7 +43,6 @@ fun getArrowBitmapByBearing(baseBitmap: Bitmap, bearing: Int): Bitmap {
canvas.save() canvas.save()
canvas.scale((bitmap.width / baseBitmap.width.toFloat()), (bitmap.height / baseBitmap.height.toFloat()), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat()) canvas.scale((bitmap.width / baseBitmap.width.toFloat()), (bitmap.height / baseBitmap.height.toFloat()), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
Log.d(KarooHeadwindExtension.TAG, "Drawing arrow at $bearingRounded")
canvas.rotate(bearingRounded.toFloat(), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat()) canvas.rotate(bearingRounded.toFloat(), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
canvas.drawBitmap(baseBitmap, ((bitmap.width - baseBitmap.width) / 2).toFloat(), ((bitmap.height - baseBitmap.height) / 2).toFloat(), paint) canvas.drawBitmap(baseBitmap, ((bitmap.width - baseBitmap.width) / 2).toFloat(), ((bitmap.height - baseBitmap.height) / 2).toFloat(), paint)
canvas.restore() canvas.restore()

View File

@ -2,9 +2,9 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.getRelativeHeadingFlow import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService

View File

@ -12,12 +12,12 @@ import androidx.core.graphics.ColorUtils
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews import androidx.glance.appwidget.GlanceRemoteViews
import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.R import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
import de.timklge.karooheadwind.getRelativeHeadingFlow import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.WindDirectionIndicatorSetting
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamDataFlow import de.timklge.karooheadwind.streamDataFlow
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings

View File

@ -2,10 +2,10 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
import de.timklge.karooheadwind.getRelativeHeadingFlow import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService

View File

@ -20,8 +20,8 @@ import androidx.glance.text.Text
import androidx.glance.text.TextAlign import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle import androidx.glance.text.TextStyle
import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.screens.HeadwindSettings
@OptIn(ExperimentalGlanceRemoteViewsApi::class) @OptIn(ExperimentalGlanceRemoteViewsApi::class)
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult { suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {

View File

@ -11,13 +11,13 @@ import androidx.glance.layout.Alignment
import androidx.glance.layout.Box import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxSize
import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.OpenMeteoData import de.timklge.karooheadwind.OpenMeteoData
import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.WeatherInterpretation import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.getHeadingFlow import de.timklge.karooheadwind.getHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.TemperatureUnit
import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile import de.timklge.karooheadwind.streamUserProfile
@ -83,7 +83,8 @@ class WeatherDataType(
0.0, 0.0, "Europe/Berlin", 30.0, 0, 0.0, 0.0, "Europe/Berlin", 30.0, 0,
null null
), HeadwindSettings())) ), HeadwindSettings()
))
delay(5_000) delay(5_000)
} }

View File

@ -20,17 +20,17 @@ import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.width import androidx.glance.layout.width
import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.HeadwindWidgetSettings
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.OpenMeteoData import de.timklge.karooheadwind.OpenMeteoData
import de.timklge.karooheadwind.OpenMeteoForecastData import de.timklge.karooheadwind.OpenMeteoForecastData
import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.UpcomingRoute import de.timklge.karooheadwind.UpcomingRoute
import de.timklge.karooheadwind.WeatherDataResponse import de.timklge.karooheadwind.WeatherDataResponse
import de.timklge.karooheadwind.WeatherInterpretation import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.getHeadingFlow import de.timklge.karooheadwind.getHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
import de.timklge.karooheadwind.screens.TemperatureUnit
import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUpcomingRoute import de.timklge.karooheadwind.streamUpcomingRoute

View File

@ -31,8 +31,8 @@ import androidx.glance.text.Text
import androidx.glance.text.TextAlign import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle import androidx.glance.text.TextStyle
import de.timklge.karooheadwind.R import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.WeatherInterpretation import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.screens.TemperatureUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.ceil import kotlin.math.ceil

View File

@ -32,7 +32,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt

View File

@ -9,7 +9,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf

View File

@ -1,135 +1,44 @@
package de.timklge.karooheadwind.screens package de.timklge.karooheadwind.screens
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.getGpsCoordinateFlow
import de.timklge.karooheadwind.saveSettings import de.timklge.karooheadwind.saveSettings
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamStats
import de.timklge.karooheadwind.streamUserProfile
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
enum class WindUnit(val id: String, val label: String, val unitDisplay: String){
KILOMETERS_PER_HOUR("kmh", "Kilometers (km/h)", "km/h"),
METERS_PER_SECOND("ms", "Meters (m/s)", "m/s"),
MILES_PER_HOUR("mph", "Miles (mph)", "mph"),
KNOTS("kn", "Knots (kn)", "kn")
}
enum class PrecipitationUnit(val id: String, val label: String, val unitDisplay: String){
MILLIMETERS("mm", "Millimeters (mm)", "mm"),
INCH("inch", "Inch", "in")
}
enum class WindDirectionIndicatorTextSetting(val id: String, val label: String){
HEADWIND_SPEED("headwind-speed", "Headwind speed"),
WIND_SPEED("absolute-wind-speed", "Absolute wind speed"),
NONE("none", "None")
}
enum class WindDirectionIndicatorSetting(val id: String, val label: String){
HEADWIND_DIRECTION("headwind-direction", "Headwind"),
WIND_DIRECTION("wind-direction", "Absolute wind direction"),
}
enum class TemperatureUnit(val id: String, val label: String, val unitDisplay: String){
CELSIUS("celsius", "Celsius (°C)", "°C"),
FAHRENHEIT("fahrenheit", "Fahrenheit (°F)", "°F")
}
enum class RoundLocationSetting(val id: String, val label: String, val km: Int){
KM_1("1 km", "1 km", 1),
KM_2("2 km", "2 km", 2),
KM_5("5 km", "5 km", 5)
}
@Serializable
data class HeadwindSettings(
val windUnit: WindUnit = WindUnit.KILOMETERS_PER_HOUR,
val welcomeDialogAccepted: Boolean = false,
val windDirectionIndicatorTextSetting: WindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.HEADWIND_SPEED,
val windDirectionIndicatorSetting: WindDirectionIndicatorSetting = WindDirectionIndicatorSetting.HEADWIND_DIRECTION,
val roundLocationTo: RoundLocationSetting = RoundLocationSetting.KM_2,
val forecastedKmPerHour: Int = 20,
val forecastedMilesPerHour: Int = 12,
){
companion object {
val defaultSettings = Json.encodeToString(HeadwindSettings())
}
fun getForecastMetersPerHour(isImperial: Boolean): Int {
return if (isImperial) forecastedMilesPerHour * 1609 else forecastedKmPerHour * 1000
}
}
@Serializable
data class HeadwindWidgetSettings(
val currentForecastHourOffset: Int = 0
){
companion object {
val defaultWidgetSettings = Json.encodeToString(HeadwindWidgetSettings())
}
}
@Serializable
data class HeadwindStats(
val lastSuccessfulWeatherRequest: Long? = null,
val lastSuccessfulWeatherPosition: GpsCoordinates? = null,
val failedWeatherRequest: Long? = null,
){
companion object {
val defaultStats = Json.encodeToString(HeadwindStats())
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainScreen(onFinish: () -> Unit) { fun MainScreen(onFinish: () -> Unit) {
var karooConnected by remember { mutableStateOf(false) } var karooConnected by remember { mutableStateOf(false) }
@ -137,30 +46,20 @@ fun MainScreen(onFinish: () -> Unit) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val karooSystem = remember { KarooSystemService(ctx) } val karooSystem = remember { KarooSystemService(ctx) }
var selectedWindUnit by remember { mutableStateOf(WindUnit.KILOMETERS_PER_HOUR) }
var welcomeDialogVisible by remember { mutableStateOf(false) } var welcomeDialogVisible by remember { mutableStateOf(false) }
var selectedWindDirectionIndicatorTextSetting by remember { mutableStateOf(WindDirectionIndicatorTextSetting.HEADWIND_SPEED) } var tabIndex by remember { mutableIntStateOf(0) }
var selectedWindDirectionIndicatorSetting by remember { mutableStateOf(WindDirectionIndicatorSetting.HEADWIND_DIRECTION) }
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_2) }
var forecastKmPerHour by remember { mutableStateOf("20") }
var forecastMilesPerHour by remember { mutableStateOf("12") }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null) val tabs = listOf("Weather", "Settings")
val stats by ctx.streamStats().collectAsStateWithLifecycle(HeadwindStats())
val location by karooSystem.getGpsCoordinateFlow(ctx).collectAsStateWithLifecycle(null)
var savedDialogVisible by remember { mutableStateOf(false) } DisposableEffect(Unit) {
var exitDialogVisible by remember { mutableStateOf(false) } onDispose {
karooSystem.disconnect()
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
ctx.streamSettings(karooSystem).collect { settings -> ctx.streamSettings(karooSystem).collect { settings ->
selectedWindUnit = settings.windUnit
welcomeDialogVisible = !settings.welcomeDialogAccepted welcomeDialogVisible = !settings.welcomeDialogAccepted
selectedWindDirectionIndicatorTextSetting = settings.windDirectionIndicatorTextSetting
selectedWindDirectionIndicatorSetting = settings.windDirectionIndicatorSetting
selectedRoundLocationSetting = settings.roundLocationTo
forecastKmPerHour = settings.forecastedKmPerHour.toString()
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
} }
} }
@ -170,153 +69,32 @@ fun MainScreen(onFinish: () -> Unit) {
} }
} }
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier Column(modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background)) { .background(MaterialTheme.colorScheme.background)) {
TopAppBar(title = { Text("Headwind") })
Column(modifier = Modifier
.padding(5.dp)
.verticalScroll(rememberScrollState())
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
val windDirectionIndicatorSettingDropdownOptions = WindDirectionIndicatorSetting.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) } Column(modifier = Modifier.fillMaxWidth()) {
val windDirectionIndicatorSettingSelection by remember(selectedWindDirectionIndicatorSetting) { TabRow(selectedTabIndex = tabIndex) {
mutableStateOf(windDirectionIndicatorSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorSetting.id }!!) tabs.forEachIndexed { index, title ->
} Tab(text = { Text(title) },
Dropdown(label = "Wind direction indicator", options = windDirectionIndicatorSettingDropdownOptions, selected = windDirectionIndicatorSettingSelection) { selectedOption -> selected = tabIndex == index,
selectedWindDirectionIndicatorSetting = WindDirectionIndicatorSetting.entries.find { unit -> unit.id == selectedOption.id }!! onClick = { tabIndex = index }
}
val windDirectionIndicatorTextSettingDropdownOptions = WindDirectionIndicatorTextSetting.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val windDirectionIndicatorTextSettingSelection by remember(selectedWindDirectionIndicatorTextSetting) {
mutableStateOf(windDirectionIndicatorTextSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorTextSetting.id }!!)
}
Dropdown(label = "Text on headwind indicator", options = windDirectionIndicatorTextSettingDropdownOptions, selected = windDirectionIndicatorTextSettingSelection) { selectedOption ->
selectedWindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.entries.find { unit -> unit.id == selectedOption.id }!!
}
val windSpeedUnitDropdownOptions = WindUnit.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val windSpeedUnitInitialSelection by remember(selectedWindUnit) {
mutableStateOf(windSpeedUnitDropdownOptions.find { option -> option.id == selectedWindUnit.id }!!)
}
Dropdown(label = "Wind Speed Unit", options = windSpeedUnitDropdownOptions, selected = windSpeedUnitInitialSelection) { selectedOption ->
selectedWindUnit = WindUnit.entries.find { unit -> unit.id == selectedOption.id }!!
}
val roundLocationDropdownOptions = RoundLocationSetting.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val roundLocationInitialSelection by remember(selectedRoundLocationSetting) {
mutableStateOf(roundLocationDropdownOptions.find { option -> option.id == selectedRoundLocationSetting.id }!!)
}
Dropdown(label = "Round Location", options = roundLocationDropdownOptions, selected = roundLocationInitialSelection) { selectedOption ->
selectedRoundLocationSetting = RoundLocationSetting.entries.find { unit -> unit.id == selectedOption.id }!!
}
if (profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL){
OutlinedTextField(value = forecastMilesPerHour, modifier = Modifier.fillMaxWidth(),
onValueChange = { forecastMilesPerHour = it },
label = { Text("Forecast Distance per Hour") },
suffix = { Text("mi") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true
)
} else {
OutlinedTextField(value = forecastKmPerHour, modifier = Modifier.fillMaxWidth(),
onValueChange = { forecastKmPerHour = it },
label = { Text("Forecast Distance per Hour") },
suffix = { Text("km") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true
)
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(50.dp), onClick = {
val newSettings = HeadwindSettings(windUnit = selectedWindUnit,
welcomeDialogAccepted = true,
windDirectionIndicatorSetting = selectedWindDirectionIndicatorSetting,
windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
roundLocationTo = selectedRoundLocationSetting,
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20)
coroutineScope.launch {
saveSettings(ctx, newSettings)
savedDialogVisible = true
}
}) {
Icon(Icons.Default.Done, contentDescription = "Save")
Spacer(modifier = Modifier.width(5.dp))
Text("Save")
}
if (!karooConnected){
Text(modifier = Modifier.padding(5.dp), text = "Could not read device status. Is your Karoo updated?")
}
val lastPosition = location?.let { l -> stats.lastSuccessfulWeatherPosition?.distanceTo(l) }
val lastPositionDistanceStr = lastPosition?.let { dist -> " (${dist.roundToInt()} km away)" } ?: ""
if (stats.failedWeatherRequest != null && (stats.lastSuccessfulWeatherRequest == null || stats.failedWeatherRequest!! > stats.lastSuccessfulWeatherRequest!!)){
val successfulTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalTime().truncatedTo(
ChronoUnit.SECONDS)
val successfulDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalDate()
val lastTryTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalTime().truncatedTo(
ChronoUnit.SECONDS)
val lastTryDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalDate()
val successStr = if(lastPosition != null) " Last data received at $successfulDate ${successfulTime}${lastPositionDistanceStr}." else ""
Text(modifier = Modifier.padding(5.dp), text = "Failed to update weather data; last try at $lastTryDate ${lastTryTime}.${successStr}")
} else if(stats.lastSuccessfulWeatherRequest != null){
val localDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalDate()
val localTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalTime().truncatedTo(
ChronoUnit.SECONDS)
Text(modifier = Modifier.padding(5.dp), text = "Last weather data received at $localDate ${localTime}${lastPositionDistanceStr}")
} else {
Text(modifier = Modifier.padding(5.dp), text = "No weather data received yet, waiting for GPS fix...")
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(50.dp), onClick = {
exitDialogVisible = true
}) {
Icon(Icons.AutoMirrored.Default.ExitToApp, contentDescription = "Exit")
Spacer(modifier = Modifier.width(5.dp))
Text("Exit")
}
}
if (exitDialogVisible) {
AlertDialog(onDismissRequest = { exitDialogVisible = false },
confirmButton = { Button(onClick = {
onFinish()
}) { Text("Yes") } },
dismissButton = { Button(onClick = {
exitDialogVisible = false
}) { Text("No") } },
text = { Text("Do you really want to exit?") }
) )
} }
} }
when (tabIndex) {
if (savedDialogVisible){ 0 -> WeatherScreen(onFinish)
AlertDialog(onDismissRequest = { savedDialogVisible = false }, 1 -> SettingsScreen(onFinish)
confirmButton = { Button(onClick = { }
savedDialogVisible = false }
}) { Text("OK") } },
text = { Text("Settings saved successfully.") }
)
} }
if (welcomeDialogVisible){ if (welcomeDialogVisible){
AlertDialog(onDismissRequest = { }, AlertDialog(onDismissRequest = { },
confirmButton = { Button(onClick = { confirmButton = { Button(onClick = {
coroutineScope.launch { coroutineScope.launch {
saveSettings(ctx, HeadwindSettings(windUnit = selectedWindUnit, saveSettings(ctx, HeadwindSettings(welcomeDialogAccepted = true))
welcomeDialogAccepted = true))
} }
}) { Text("OK") } }, }) { Text("OK") } },
text = { text = {
@ -334,4 +112,15 @@ fun MainScreen(onFinish: () -> Unit) {
} }
) )
} }
Image(
painter = painterResource(id = R.drawable.back),
contentDescription = "Back",
modifier = Modifier
.align(Alignment.BottomStart)
.padding(bottom = 10.dp)
.size(54.dp)
.clickable { onFinish() }
)
}
} }

View File

@ -0,0 +1,217 @@
package de.timklge.karooheadwind.screens
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.RoundLocationSetting
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
import de.timklge.karooheadwind.WindUnit
import de.timklge.karooheadwind.saveSettings
import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@Composable
fun SettingsScreen(onFinish: () -> Unit) {
var karooConnected by remember { mutableStateOf(false) }
val ctx = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val karooSystem = remember { KarooSystemService(ctx) }
var selectedWindUnit by remember { mutableStateOf(WindUnit.KILOMETERS_PER_HOUR) }
var selectedWindDirectionIndicatorTextSetting by remember {
mutableStateOf(
WindDirectionIndicatorTextSetting.HEADWIND_SPEED
)
}
var selectedWindDirectionIndicatorSetting by remember {
mutableStateOf(
WindDirectionIndicatorSetting.HEADWIND_DIRECTION
)
}
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_2) }
var forecastKmPerHour by remember { mutableStateOf("20") }
var forecastMilesPerHour by remember { mutableStateOf("12") }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
LaunchedEffect(Unit) {
ctx.streamSettings(karooSystem).collect { settings ->
selectedWindUnit = settings.windUnit
selectedWindDirectionIndicatorTextSetting = settings.windDirectionIndicatorTextSetting
selectedWindDirectionIndicatorSetting = settings.windDirectionIndicatorSetting
selectedRoundLocationSetting = settings.roundLocationTo
forecastKmPerHour = settings.forecastedKmPerHour.toString()
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
}
}
LaunchedEffect(Unit) {
karooSystem.connect { connected ->
karooConnected = connected
}
}
DisposableEffect(Unit) {
onDispose {
karooSystem.disconnect()
}
}
suspend fun updateSettings(){
Log.d(KarooHeadwindExtension.TAG, "Saving settings")
val newSettings = HeadwindSettings(
windUnit = selectedWindUnit,
welcomeDialogAccepted = true,
windDirectionIndicatorSetting = selectedWindDirectionIndicatorSetting,
windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
roundLocationTo = selectedRoundLocationSetting,
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20
)
saveSettings(ctx, newSettings)
}
DisposableEffect(Unit) {
onDispose {
runBlocking {
updateSettings()
}
}
}
BackHandler {
coroutineScope.launch {
updateSettings()
onFinish()
}
}
Column(
modifier = Modifier
.padding(5.dp)
.verticalScroll(rememberScrollState())
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)
) {
val windDirectionIndicatorSettingDropdownOptions =
WindDirectionIndicatorSetting.entries.toList()
.map { unit -> DropdownOption(unit.id, unit.label) }
val windDirectionIndicatorSettingSelection by remember(selectedWindDirectionIndicatorSetting) {
mutableStateOf(windDirectionIndicatorSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorSetting.id }!!)
}
Dropdown(
label = "Wind direction indicator",
options = windDirectionIndicatorSettingDropdownOptions,
selected = windDirectionIndicatorSettingSelection
) { selectedOption ->
selectedWindDirectionIndicatorSetting =
WindDirectionIndicatorSetting.entries.find { unit -> unit.id == selectedOption.id }!!
}
val windDirectionIndicatorTextSettingDropdownOptions =
WindDirectionIndicatorTextSetting.entries.toList()
.map { unit -> DropdownOption(unit.id, unit.label) }
val windDirectionIndicatorTextSettingSelection by remember(
selectedWindDirectionIndicatorTextSetting
) {
mutableStateOf(windDirectionIndicatorTextSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorTextSetting.id }!!)
}
Dropdown(
label = "Text on headwind indicator",
options = windDirectionIndicatorTextSettingDropdownOptions,
selected = windDirectionIndicatorTextSettingSelection
) { selectedOption ->
selectedWindDirectionIndicatorTextSetting =
WindDirectionIndicatorTextSetting.entries.find { unit -> unit.id == selectedOption.id }!!
}
val windSpeedUnitDropdownOptions =
WindUnit.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val windSpeedUnitInitialSelection by remember(selectedWindUnit) {
mutableStateOf(windSpeedUnitDropdownOptions.find { option -> option.id == selectedWindUnit.id }!!)
}
Dropdown(
label = "Wind Speed Unit",
options = windSpeedUnitDropdownOptions,
selected = windSpeedUnitInitialSelection
) { selectedOption ->
selectedWindUnit = WindUnit.entries.find { unit -> unit.id == selectedOption.id }!!
}
val roundLocationDropdownOptions = RoundLocationSetting.entries.toList()
.map { unit -> DropdownOption(unit.id, unit.label) }
val roundLocationInitialSelection by remember(selectedRoundLocationSetting) {
mutableStateOf(roundLocationDropdownOptions.find { option -> option.id == selectedRoundLocationSetting.id }!!)
}
Dropdown(
label = "Round Location",
options = roundLocationDropdownOptions,
selected = roundLocationInitialSelection
) { selectedOption ->
selectedRoundLocationSetting =
RoundLocationSetting.entries.find { unit -> unit.id == selectedOption.id }!!
}
if (profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) {
OutlinedTextField(
value = forecastMilesPerHour, modifier = Modifier.fillMaxWidth(),
onValueChange = { forecastMilesPerHour = it },
label = { Text("Forecast Distance per Hour") },
suffix = { Text("mi") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true
)
} else {
OutlinedTextField(
value = forecastKmPerHour, modifier = Modifier.fillMaxWidth(),
onValueChange = { forecastKmPerHour = it },
label = { Text("Forecast Distance per Hour") },
suffix = { Text("km") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true
)
}
if (!karooConnected) {
Text(
modifier = Modifier.padding(5.dp),
text = "Could not read device status. Is your Karoo updated?"
)
}
Spacer(modifier = Modifier.padding(30.dp))
}
}

View File

@ -0,0 +1,234 @@
package de.timklge.karooheadwind.screens
import android.graphics.BitmapFactory
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.timklge.karooheadwind.HeadwindStats
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.PrecipitationUnit
import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
import de.timklge.karooheadwind.getGpsCoordinateFlow
import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamStats
import de.timklge.karooheadwind.streamUpcomingRoute
import de.timklge.karooheadwind.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
@Composable
fun WeatherScreen(onFinish: () -> Unit) {
var karooConnected by remember { mutableStateOf<Boolean?>(null) }
val ctx = LocalContext.current
val karooSystem = remember { KarooSystemService(ctx) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
val stats by ctx.streamStats().collectAsStateWithLifecycle(HeadwindStats())
val location by karooSystem.getGpsCoordinateFlow(ctx).collectAsStateWithLifecycle(null)
val weatherData by ctx.streamCurrentWeatherData().collectAsStateWithLifecycle(emptyList())
val baseBitmap = BitmapFactory.decodeResource(
ctx.resources,
R.drawable.arrow_0
)
LaunchedEffect(Unit) {
karooSystem.connect { connected ->
karooConnected = connected
}
}
DisposableEffect(Unit) {
onDispose {
karooSystem.disconnect()
}
}
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(5.dp)) {
if (karooConnected == false) {
Text(
modifier = Modifier.padding(5.dp),
text = "Could not read device status. Is your Karoo updated?"
)
}
val currentWeatherData = weatherData.firstOrNull()?.data
val requestedWeatherPosition = weatherData.firstOrNull()?.requestedPosition
val formattedTime = currentWeatherData?.let { timeFormatter.format(Instant.ofEpochSecond(currentWeatherData.current.time)) }
val formattedDate = currentWeatherData?.let { Instant.ofEpochSecond(currentWeatherData.current.time).atZone(ZoneId.systemDefault()).toLocalDate().format(
DateTimeFormatter.ofLocalizedDate(
FormatStyle.SHORT))
}
if (karooConnected == true && currentWeatherData != null) {
WeatherWidget(
dateLabel = formattedDate,
timeLabel = formattedTime,
baseBitmap = baseBitmap,
current = WeatherInterpretation.fromWeatherCode(currentWeatherData.current.weatherCode),
windBearing = currentWeatherData.current.windDirection.roundToInt(),
windSpeed = currentWeatherData.current.windSpeed.roundToInt(),
windGusts = currentWeatherData.current.windGusts.roundToInt(),
precipitation = currentWeatherData.current.precipitation,
temperature = currentWeatherData.current.temperature.toInt(),
temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL,
precipitationUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH,
distance = requestedWeatherPosition?.let { l -> location?.distanceTo(l)?.times(1000) },
includeDistanceLabel = false,
)
}
val lastPosition = location?.let { l -> stats.lastSuccessfulWeatherPosition?.distanceTo(l) }
val lastPositionDistanceStr =
lastPosition?.let { dist -> " (${dist.roundToInt()} km away)" } ?: ""
if (stats.failedWeatherRequest != null && (stats.lastSuccessfulWeatherRequest == null || stats.failedWeatherRequest!! > stats.lastSuccessfulWeatherRequest!!)) {
val successfulTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(
stats.lastSuccessfulWeatherRequest ?: 0
), ZoneOffset.systemDefault()
).toLocalTime().truncatedTo(
ChronoUnit.SECONDS
)
val successfulDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(
stats.lastSuccessfulWeatherRequest ?: 0
), ZoneOffset.systemDefault()
).toLocalDate()
val lastTryTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0),
ZoneOffset.systemDefault()
).toLocalTime().truncatedTo(
ChronoUnit.SECONDS
)
val lastTryDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0),
ZoneOffset.systemDefault()
).toLocalDate()
val successStr =
if (lastPosition != null) " Last data received at $successfulDate ${successfulTime}${lastPositionDistanceStr}." else ""
Text(
modifier = Modifier.padding(5.dp),
text = "Failed to update weather data; last try at $lastTryDate ${lastTryTime}.${successStr}"
)
} else if (stats.lastSuccessfulWeatherRequest != null) {
val localDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(
stats.lastSuccessfulWeatherRequest ?: 0
), ZoneOffset.systemDefault()
).toLocalDate()
val localTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(
stats.lastSuccessfulWeatherRequest ?: 0
), ZoneOffset.systemDefault()
).toLocalTime().truncatedTo(
ChronoUnit.SECONDS
)
Text(
modifier = Modifier.padding(5.dp),
text = "Last weather data received at $localDate ${localTime}${lastPositionDistanceStr}"
)
} else {
Text(
modifier = Modifier.padding(5.dp),
text = "No weather data received yet, waiting for GPS fix..."
)
}
val upcomingRoute by karooSystem.streamUpcomingRoute().collectAsStateWithLifecycle(null)
for (index in 1..12){
val positionIndex = if (weatherData.size == 1) 0 else index
if (weatherData.getOrNull(positionIndex) == null) break
if (index >= (weatherData.getOrNull(positionIndex)?.data?.forecastData?.weatherCode?.size ?: 0)) {
break
}
val data = weatherData.getOrNull(positionIndex)?.data
val distanceAlongRoute = weatherData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
val position = weatherData.getOrNull(positionIndex)?.requestedPosition?.let { "${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}" }
Log.d(KarooHeadwindExtension.TAG, "Distance along route index ${positionIndex}: $position")
if (index > 1) {
Spacer(
modifier = Modifier
.fillMaxWidth()
.background(
Color.Black
)
.height(1.dp)
)
}
val distanceFromCurrent = upcomingRoute?.distanceAlongRoute?.let { currentDistanceAlongRoute ->
distanceAlongRoute?.minus(currentDistanceAlongRoute)
}
val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(index) ?: 0)
val unixTime = data?.forecastData?.time?.get(index) ?: 0
val formattedForecastTime = WeatherForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
val formattedForecastDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
WeatherWidget(
baseBitmap,
current = interpretation,
windBearing = data?.forecastData?.windDirection?.get(index)?.roundToInt() ?: 0,
windSpeed = data?.forecastData?.windSpeed?.get(index)?.roundToInt() ?: 0,
windGusts = data?.forecastData?.windGusts?.get(index)?.roundToInt() ?: 0,
precipitation = data?.forecastData?.precipitation?.get(index) ?: 0.0,
precipitationProbability = data?.forecastData?.precipitationProbability?.get(index) ?: 0,
temperature = data?.forecastData?.temperature?.get(index)?.roundToInt() ?: 0,
temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
timeLabel = formattedForecastTime,
dateLabel = formattedForecastDate,
distance = distanceFromCurrent,
isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL,
precipitationUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH,
includeDistanceLabel = true
)
}
Spacer(modifier = Modifier.padding(30.dp))
}
}

View File

@ -0,0 +1,168 @@
package de.timklge.karooheadwind.screens
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.timklge.karooheadwind.PrecipitationUnit
import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.datatypes.getArrowBitmapByBearing
import de.timklge.karooheadwind.datatypes.getWeatherIcon
import kotlin.math.absoluteValue
import kotlin.math.ceil
@Composable
fun WeatherWidget(
baseBitmap: Bitmap,
current: WeatherInterpretation,
windBearing: Int,
windSpeed: Int,
windGusts: Int,
precipitation: Double,
temperature: Int,
temperatureUnit: TemperatureUnit,
timeLabel: String? = null,
dateLabel: String? = null,
distance: Double? = null,
includeDistanceLabel: Boolean = false,
precipitationProbability: Int? = null,
precipitationUnit: PrecipitationUnit,
isImperial: Boolean
) {
Row(
modifier = Modifier.fillMaxWidth().padding(5.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
if (dateLabel != null) {
Text(
text = dateLabel,
style = TextStyle(
fontSize = 14.sp
)
)
}
if (distance != null) {
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
val text = if (includeDistanceLabel){
if(distanceInUserUnit > 0){
"In $label"
} else {
"$label ago"
}
} else {
label
}
Text(
text = text,
style = TextStyle(
fontSize = 14.sp
)
)
}
if (timeLabel != null) {
Text(
text = timeLabel,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
)
}
}
// Weather icon (larger)
Icon(
painter = painterResource(id = getWeatherIcon(current)),
contentDescription = "Current weather",
modifier = Modifier.size(72.dp)
)
Column {
// Temperature (larger)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.thermometer),
contentDescription = "Temperature",
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${temperature}${temperatureUnit.unitDisplay}",
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
)
}
// Precipitation
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 4.dp)
) {
val precipitationProbabilityLabel =
if (precipitationProbability != null) "${precipitationProbability}% " else ""
Text(
text = "${precipitationProbabilityLabel}${ceil(precipitation).toInt()}${precipitationUnit.unitDisplay}",
style = TextStyle(
fontSize = 14.sp
)
)
}
// Wind
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 4.dp)
) {
Image(
bitmap = getArrowBitmapByBearing(baseBitmap, windBearing).asImageBitmap(),
colorFilter = ColorFilter.tint(Color.Black),
contentDescription = "Wind direction",
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "$windSpeed,$windGusts",
style = TextStyle(
fontSize = 14.sp
)
)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB