diff --git a/README.md b/README.md
index 160617b..e826031 100644
--- a/README.md
+++ b/README.md
@@ -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.
+
+



diff --git a/app/manifest.json b/app/manifest.json
index eea0770..be27c83 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -7,5 +7,5 @@
"latestVersionCode": 14,
"developer": "timklge",
"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"
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/DataStore.kt b/app/src/main/kotlin/de/timklge/karooheadwind/DataStore.kt
index a3aec3f..d1aab68 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/DataStore.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/DataStore.kt
@@ -8,10 +8,6 @@ import com.mapbox.geojson.LineString
import com.mapbox.turf.TurfConstants
import com.mapbox.turf.TurfMeasurement
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.models.DataType
import io.hammerhead.karooext.models.OnNavigationState
@@ -21,16 +17,13 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.Serializable
-import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -102,7 +95,8 @@ fun Context.streamSettings(karooSystemService: KarooSystemService): Flow(settingsJson[settingsKey]!!)
} else {
- val defaultSettings = jsonWithUnknownKeys.decodeFromString(HeadwindSettings.defaultSettings)
+ val defaultSettings = jsonWithUnknownKeys.decodeFromString(
+ HeadwindSettings.defaultSettings)
val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit
@@ -138,6 +132,7 @@ fun KarooSystemService.streamUpcomingRoute(): Flow {
navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) }
}
.combine(distanceToDestinationStream) { routePolyline, distanceToDestination ->
+ Log.d(KarooHeadwindExtension.TAG, "Route polyline size: ${routePolyline?.coordinates()?.size}, distance to destination: $distanceToDestination")
if (routePolyline != null){
val length = TurfMeasurement.length(routePolyline, TurfConstants.UNIT_METERS)
if (routePolyline != lastKnownRoutePolyline){
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/Extensions.kt b/app/src/main/kotlin/de/timklge/karooheadwind/Extensions.kt
index a6f398c..53359bd 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/Extensions.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/Extensions.kt
@@ -1,67 +1,15 @@
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.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.OnNavigationState
import io.hammerhead.karooext.models.OnStreamState
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.trySendBlocking
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
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.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 {
return callbackFlow {
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/HeadingFlow.kt b/app/src/main/kotlin/de/timklge/karooheadwind/HeadingFlow.kt
index 7b1c750..50f9885 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/HeadingFlow.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/HeadingFlow.kt
@@ -91,7 +91,10 @@ suspend fun KarooSystemService.updateLastKnownGps(context: Context) {
}
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow {
- // return flowOf(GpsCoordinates(52.5164069,13.3784))
+ /* return flow {
+ emit(GpsCoordinates(52.5164069,13.3784))
+ awaitCancellation()
+ } */
val initialFlow = flow {
val lastKnownPosition = context.getLastKnownPosition()
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/HeadwindSettings.kt b/app/src/main/kotlin/de/timklge/karooheadwind/HeadwindSettings.kt
new file mode 100644
index 0000000..8864399
--- /dev/null
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/HeadwindSettings.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt
index cc0207c..fbd8cbb 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt
@@ -7,13 +7,13 @@ import com.mapbox.turf.TurfConstants
import com.mapbox.turf.TurfMeasurement
import de.timklge.karooheadwind.datatypes.CloudCoverDataType
import de.timklge.karooheadwind.datatypes.GpsCoordinates
+import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
import de.timklge.karooheadwind.datatypes.PrecipitationDataType
import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
+import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
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.UserWindSpeedDataType
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.WindGustsDataType
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.extension.KarooExtension
import io.hammerhead.karooext.models.UserProfile
@@ -47,7 +44,6 @@ import kotlinx.coroutines.withContext
import java.time.Duration
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
-import java.util.Locale
import java.util.zip.GZIPInputStream
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@@ -159,27 +155,46 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
ChronoUnit.MILLIS.between(startOfHour, now)
}
val msToNextFullHour = (1_000 * 60 * 60) - msSinceFullHour
- val calculatedDistanceToNextFullHour = (msToNextFullHour / (1_000.0 * 60 * 60)) * distancePerHour
- val distanceToNextFullHour = if (calculatedDistanceToNextFullHour > 5_000) calculatedDistanceToNextFullHour else distancePerHour
+ val calculatedDistanceToNextFullHour = ((msToNextFullHour / (1_000.0 * 60 * 60)) * distancePerHour).coerceIn(0.0, 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 {
add(gps)
- var currentPosition = positionOnRoute + distanceToNextFullHour
+ var currentPosition = positionOnRoute + calculatedDistanceToNextFullHour
var lastRequestedPosition = currentPosition
- while (currentPosition < upcomingRoute.routeLength && size < 10){
- val point = TurfMeasurement.along(upcomingRoute.routePolyline, currentPosition, TurfConstants.UNIT_METERS)
- add(GpsCoordinates(point.latitude(), point.longitude(), distanceAlongRoute = currentPosition))
+ while (currentPosition < upcomingRoute.routeLength && size < 10) {
+ val point = TurfMeasurement.along(
+ upcomingRoute.routePolyline,
+ currentPosition,
+ TurfConstants.UNIT_METERS
+ )
+ add(
+ GpsCoordinates(
+ point.latitude(),
+ point.longitude(),
+ distanceAlongRoute = currentPosition
+ )
+ )
lastRequestedPosition = currentPosition
currentPosition += distancePerHour
}
- if (upcomingRoute.routeLength > lastRequestedPosition + 5_000){
- val point = TurfMeasurement.along(upcomingRoute.routePolyline, upcomingRoute.routeLength, TurfConstants.UNIT_METERS)
- add(GpsCoordinates(point.latitude(), point.longitude(), distanceAlongRoute = upcomingRoute.routeLength))
+ if (upcomingRoute.routeLength > lastRequestedPosition + 5_000) {
+ val point = TurfMeasurement.along(
+ upcomingRoute.routePolyline,
+ upcomingRoute.routeLength,
+ TurfConstants.UNIT_METERS
+ )
+ add(
+ GpsCoordinates(
+ point.latitude(),
+ point.longitude(),
+ distanceAlongRoute = upcomingRoute.routeLength
+ )
+ )
}
}
} else {
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt b/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt
index efd98dc..b4c4914 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt
@@ -2,9 +2,6 @@ package de.timklge.karooheadwind
import android.util.Log
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.models.HttpResponseState
import io.hammerhead.karooext.models.OnHttpResponse
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindDirectionDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindDirectionDataType.kt
index 1b5ed9d..ece3670 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindDirectionDataType.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindDirectionDataType.kt
@@ -7,12 +7,11 @@ import androidx.compose.ui.unit.DpSize
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews
import de.timklge.karooheadwind.HeadingResponse
+import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension
+import de.timklge.karooheadwind.WindDirectionIndicatorSetting
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.streamDataFlow
import de.timklge.karooheadwind.streamSettings
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl
@@ -30,8 +29,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindDirectionView.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindDirectionView.kt
index b529dde..b0c8898 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindDirectionView.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindDirectionView.kt
@@ -43,7 +43,6 @@ fun getArrowBitmapByBearing(baseBitmap: Bitmap, bearing: Int): Bitmap {
canvas.save()
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.drawBitmap(baseBitmap, ((bitmap.width - baseBitmap.width) / 2).toFloat(), ((bitmap.height - baseBitmap.height) / 2).toFloat(), paint)
canvas.restore()
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindSpeedDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindSpeedDataType.kt
index baf67bb..6e19dbb 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindSpeedDataType.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindSpeedDataType.kt
@@ -2,9 +2,9 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context
import de.timklge.karooheadwind.HeadingResponse
+import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.getRelativeHeadingFlow
-import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamSettings
import io.hammerhead.karooext.KarooSystemService
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TailwindAndRideSpeedDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TailwindAndRideSpeedDataType.kt
index fdd814b..4247cf8 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TailwindAndRideSpeedDataType.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TailwindAndRideSpeedDataType.kt
@@ -12,12 +12,12 @@ import androidx.core.graphics.ColorUtils
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews
import de.timklge.karooheadwind.HeadingResponse
+import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.R
+import de.timklge.karooheadwind.WindDirectionIndicatorSetting
+import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
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.streamDataFlow
import de.timklge.karooheadwind.streamSettings
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/UserWindSpeedDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/UserWindSpeedDataType.kt
index d4be500..062cf7d 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/UserWindSpeedDataType.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/UserWindSpeedDataType.kt
@@ -2,10 +2,10 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context
import de.timklge.karooheadwind.HeadingResponse
+import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
+import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
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.streamSettings
import io.hammerhead.karooext.KarooSystemService
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/Views.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/Views.kt
index f1d3dec..a20a6a6 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/Views.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/Views.kt
@@ -20,8 +20,8 @@ import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import de.timklge.karooheadwind.HeadingResponse
+import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension
-import de.timklge.karooheadwind.screens.HeadwindSettings
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt
index 46bd897..f85dbdc 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt
@@ -11,13 +11,13 @@ import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import de.timklge.karooheadwind.HeadingResponse
+import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.OpenMeteoData
+import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.WeatherInterpretation
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.streamSettings
import de.timklge.karooheadwind.streamUserProfile
@@ -83,7 +83,8 @@ class WeatherDataType(
0.0, 0.0, "Europe/Berlin", 30.0, 0,
null
- ), HeadwindSettings()))
+ ), HeadwindSettings()
+ ))
delay(5_000)
}
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt
index 11a1431..d31d1f8 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt
@@ -20,17 +20,17 @@ import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.width
import de.timklge.karooheadwind.HeadingResponse
+import de.timklge.karooheadwind.HeadwindSettings
+import de.timklge.karooheadwind.HeadwindWidgetSettings
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.OpenMeteoData
import de.timklge.karooheadwind.OpenMeteoForecastData
+import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.UpcomingRoute
import de.timklge.karooheadwind.WeatherDataResponse
import de.timklge.karooheadwind.WeatherInterpretation
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.streamSettings
import de.timklge.karooheadwind.streamUpcomingRoute
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt
index 05a990f..95e3435 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt
@@ -31,8 +31,8 @@ import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import de.timklge.karooheadwind.R
+import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.WeatherInterpretation
-import de.timklge.karooheadwind.screens.TemperatureUnit
import kotlin.math.absoluteValue
import kotlin.math.ceil
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindDirectionDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindDirectionDataType.kt
index f074192..5b35103 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindDirectionDataType.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindDirectionDataType.kt
@@ -32,7 +32,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/Dropdown.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/Dropdown.kt
index 7dbe519..936771e 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/Dropdown.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/Dropdown.kt
@@ -9,7 +9,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/MainScreen.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/MainScreen.kt
index 93af9e0..f306277 100644
--- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/MainScreen.kt
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/MainScreen.kt
@@ -1,135 +1,44 @@
package de.timklge.karooheadwind.screens
+import androidx.compose.foundation.Image
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.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.layout.width
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
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.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.OutlinedTextField
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.lifecycle.compose.collectAsStateWithLifecycle
-import de.timklge.karooheadwind.datatypes.GpsCoordinates
-import de.timklge.karooheadwind.getGpsCoordinateFlow
+import de.timklge.karooheadwind.HeadwindSettings
+import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.saveSettings
import de.timklge.karooheadwind.streamSettings
-import de.timklge.karooheadwind.streamStats
-import de.timklge.karooheadwind.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
-import io.hammerhead.karooext.models.UserProfile
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
fun MainScreen(onFinish: () -> Unit) {
var karooConnected by remember { mutableStateOf(false) }
@@ -137,30 +46,20 @@ fun MainScreen(onFinish: () -> Unit) {
val coroutineScope = rememberCoroutineScope()
val karooSystem = remember { KarooSystemService(ctx) }
- var selectedWindUnit by remember { mutableStateOf(WindUnit.KILOMETERS_PER_HOUR) }
var welcomeDialogVisible by remember { mutableStateOf(false) }
- 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") }
+ var tabIndex by remember { mutableIntStateOf(0) }
- val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
- val stats by ctx.streamStats().collectAsStateWithLifecycle(HeadwindStats())
- val location by karooSystem.getGpsCoordinateFlow(ctx).collectAsStateWithLifecycle(null)
+ val tabs = listOf("Weather", "Settings")
- var savedDialogVisible by remember { mutableStateOf(false) }
- var exitDialogVisible by remember { mutableStateOf(false) }
+ DisposableEffect(Unit) {
+ onDispose {
+ karooSystem.disconnect()
+ }
+ }
LaunchedEffect(Unit) {
ctx.streamSettings(karooSystem).collect { settings ->
- selectedWindUnit = settings.windUnit
welcomeDialogVisible = !settings.welcomeDialogAccepted
- selectedWindDirectionIndicatorTextSetting = settings.windDirectionIndicatorTextSetting
- selectedWindDirectionIndicatorSetting = settings.windDirectionIndicatorSetting
- selectedRoundLocationSetting = settings.roundLocationTo
- forecastKmPerHour = settings.forecastedKmPerHour.toString()
- forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
}
}
@@ -170,168 +69,58 @@ fun MainScreen(onFinish: () -> Unit) {
}
}
- Column(modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background)) {
- TopAppBar(title = { Text("Headwind") })
+ Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier
- .padding(5.dp)
- .verticalScroll(rememberScrollState())
- .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
- 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
- )
- }
-
-
- 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
+ Column(modifier = Modifier.fillMaxWidth()) {
+ TabRow(selectedTabIndex = tabIndex) {
+ tabs.forEachIndexed { index, title ->
+ Tab(text = { Text(title) },
+ selected = tabIndex == index,
+ onClick = { tabIndex = index }
+ )
}
- }) {
- 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")
+ }
+ when (tabIndex) {
+ 0 -> WeatherScreen(onFinish)
+ 1 -> SettingsScreen(onFinish)
+ }
}
}
- if (exitDialogVisible) {
- AlertDialog(onDismissRequest = { exitDialogVisible = false },
+ if (welcomeDialogVisible){
+ AlertDialog(onDismissRequest = { },
confirmButton = { Button(onClick = {
- onFinish()
- }) { Text("Yes") } },
- dismissButton = { Button(onClick = {
- exitDialogVisible = false
- }) { Text("No") } },
- text = { Text("Do you really want to exit?") }
+ coroutineScope.launch {
+ saveSettings(ctx, HeadwindSettings(welcomeDialogAccepted = true))
+ }
+ }) { Text("OK") } },
+ text = {
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+ Text("Welcome to karoo-headwind!")
+
+ Spacer(Modifier.padding(10.dp))
+
+ Text("You can add headwind direction and other fields to your data pages in your profile settings.")
+
+ Spacer(Modifier.padding(10.dp))
+
+ Text("Please note that this app periodically fetches data from the Open-Meteo API to know the current weather at your approximate location.")
+ }
+ }
)
}
- }
- if (savedDialogVisible){
- AlertDialog(onDismissRequest = { savedDialogVisible = false },
- confirmButton = { Button(onClick = {
- savedDialogVisible = false
- }) { Text("OK") } },
- text = { Text("Settings saved successfully.") }
- )
- }
-
- if (welcomeDialogVisible){
- AlertDialog(onDismissRequest = { },
- confirmButton = { Button(onClick = {
- coroutineScope.launch {
- saveSettings(ctx, HeadwindSettings(windUnit = selectedWindUnit,
- welcomeDialogAccepted = true))
- }
- }) { Text("OK") } },
- text = {
- Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
- Text("Welcome to karoo-headwind!")
-
- Spacer(Modifier.padding(10.dp))
-
- Text("You can add headwind direction and other fields to your data pages in your profile settings.")
-
- Spacer(Modifier.padding(10.dp))
-
- Text("Please note that this app periodically fetches data from the Open-Meteo API to know the current weather at your approximate location.")
- }
- }
+ Image(
+ painter = painterResource(id = R.drawable.back),
+ contentDescription = "Back",
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(bottom = 10.dp)
+ .size(54.dp)
+ .clickable { onFinish() }
)
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/SettingsScreen.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/SettingsScreen.kt
new file mode 100644
index 0000000..5ea81ad
--- /dev/null
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/SettingsScreen.kt
@@ -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))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt
new file mode 100644
index 0000000..207fbfc
--- /dev/null
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt
@@ -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(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))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt
new file mode 100644
index 0000000..47f9b7b
--- /dev/null
+++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt
@@ -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
+ )
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/back.png b/app/src/main/res/drawable/back.png
new file mode 100644
index 0000000..38d43cf
Binary files /dev/null and b/app/src/main/res/drawable/back.png differ