parent
4cd6d27aa2
commit
aaeb1204bf
@ -15,8 +15,8 @@ android {
|
|||||||
applicationId = "de.timklge.karooheadwind"
|
applicationId = "de.timklge.karooheadwind"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 14
|
versionCode = 15
|
||||||
versionName = "1.3"
|
versionName = "1.3.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
"packageName": "de.timklge.karooheadwind",
|
"packageName": "de.timklge.karooheadwind",
|
||||||
"iconUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png",
|
"iconUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png",
|
||||||
"latestApkUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk",
|
"latestApkUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk",
|
||||||
"latestVersion": "1.3",
|
"latestVersion": "1.3.1",
|
||||||
"latestVersionCode": 14,
|
"latestVersionCode": 15,
|
||||||
"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\n* Show current weather in app menu"
|
"releaseNotes": "* Forecast weather along route in fixed intervals if route is loaded\n* Show current weather in app menu\n*Add individual forecast fields"
|
||||||
}
|
}
|
||||||
@ -69,6 +69,7 @@ data class HeadwindSettings(
|
|||||||
val forecastedKmPerHour: Int = 20,
|
val forecastedKmPerHour: Int = 20,
|
||||||
val forecastedMilesPerHour: Int = 12,
|
val forecastedMilesPerHour: Int = 12,
|
||||||
val lastUpdateRequested: Long? = null,
|
val lastUpdateRequested: Long? = null,
|
||||||
|
val showDistanceInForecast: Boolean = true,
|
||||||
){
|
){
|
||||||
companion object {
|
companion object {
|
||||||
val defaultSettings = Json.encodeToString(HeadwindSettings())
|
val defaultSettings = Json.encodeToString(HeadwindSettings())
|
||||||
|
|||||||
@ -7,18 +7,22 @@ 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.GraphicalForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
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.PrecipitationForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
|
import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
|
||||||
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
|
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.TemperatureDataType
|
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
||||||
|
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType
|
import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
||||||
|
import de.timklge.karooheadwind.datatypes.WindForecastDataType
|
||||||
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 io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
@ -50,7 +54,7 @@ import kotlin.math.roundToInt
|
|||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
|
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3.1") {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "karoo-headwind"
|
const val TAG = "karoo-headwind"
|
||||||
}
|
}
|
||||||
@ -66,7 +70,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
|
|||||||
TailwindAndRideSpeedDataType(karooSystem, applicationContext),
|
TailwindAndRideSpeedDataType(karooSystem, applicationContext),
|
||||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||||
WeatherDataType(karooSystem, applicationContext),
|
WeatherDataType(karooSystem, applicationContext),
|
||||||
WeatherForecastDataType(karooSystem, applicationContext),
|
WeatherForecastDataType(karooSystem),
|
||||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||||
RelativeHumidityDataType(applicationContext),
|
RelativeHumidityDataType(applicationContext),
|
||||||
CloudCoverDataType(applicationContext),
|
CloudCoverDataType(applicationContext),
|
||||||
@ -77,7 +81,11 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
|
|||||||
PrecipitationDataType(applicationContext),
|
PrecipitationDataType(applicationContext),
|
||||||
SurfacePressureDataType(applicationContext),
|
SurfacePressureDataType(applicationContext),
|
||||||
SealevelPressureDataType(applicationContext),
|
SealevelPressureDataType(applicationContext),
|
||||||
UserWindSpeedDataType(karooSystem, applicationContext)
|
UserWindSpeedDataType(karooSystem, applicationContext),
|
||||||
|
TemperatureForecastDataType(karooSystem),
|
||||||
|
PrecipitationForecastDataType(karooSystem),
|
||||||
|
WindForecastDataType(karooSystem),
|
||||||
|
GraphicalForecastDataType(karooSystem)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,359 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.action.clickable
|
||||||
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
|
import androidx.glance.appwidget.action.actionRunCallback
|
||||||
|
import androidx.glance.background
|
||||||
|
import androidx.glance.color.ColorProvider
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.Spacer
|
||||||
|
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.R
|
||||||
|
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.streamCurrentWeatherData
|
||||||
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||||
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
|
import de.timklge.karooheadwind.streamWidgetSettings
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
|
import io.hammerhead.karooext.internal.ViewEmitter
|
||||||
|
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||||
|
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
import io.hammerhead.karooext.models.ViewConfig
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
abstract class ForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
|
||||||
|
@Composable
|
||||||
|
abstract fun RenderWidget(arrowBitmap: Bitmap,
|
||||||
|
current: WeatherInterpretation,
|
||||||
|
windBearing: Int,
|
||||||
|
windSpeed: Int,
|
||||||
|
windGusts: Int,
|
||||||
|
precipitation: Double,
|
||||||
|
precipitationProbability: Int?,
|
||||||
|
temperature: Int,
|
||||||
|
temperatureUnit: TemperatureUnit,
|
||||||
|
timeLabel: String,
|
||||||
|
dateLabel: String?,
|
||||||
|
distance: Double?,
|
||||||
|
isImperial: Boolean)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
private val glance = GlanceRemoteViews()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StreamData(val data: List<WeatherDataResponse>?, val settings: SettingsAndProfile,
|
||||||
|
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null,
|
||||||
|
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null)
|
||||||
|
|
||||||
|
data class SettingsAndProfile(val settings: HeadwindSettings, val isImperial: Boolean)
|
||||||
|
|
||||||
|
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
|
||||||
|
flow {
|
||||||
|
val settingsAndProfile = settingsAndProfileStream.firstOrNull()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val data = (0..<10).map { index ->
|
||||||
|
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
|
||||||
|
val forecastTimes = (0..<12).map { timeAtFullHour + it * 60 * 60 }
|
||||||
|
val forecastTemperatures = (0..<12).map { 20.0 + (-20..20).random() }
|
||||||
|
val forecastPrecipitationPropability = (0..<12).map { (0..100).random() }
|
||||||
|
val forecastPrecipitation = (0..<12).map { 0.0 + (0..10).random() }
|
||||||
|
val forecastWeatherCodes =
|
||||||
|
(0..<12).map { WeatherInterpretation.getKnownWeatherCodes().random() }
|
||||||
|
val forecastWindSpeed = (0..<12).map { 0.0 + (0..10).random() }
|
||||||
|
val forecastWindDirection = (0..<12).map { 0.0 + (0..360).random() }
|
||||||
|
val forecastWindGusts = (0..<12).map { 0.0 + (0..10).random() }
|
||||||
|
val weatherData = OpenMeteoCurrentWeatherResponse(
|
||||||
|
OpenMeteoData(
|
||||||
|
Instant.now().epochSecond,
|
||||||
|
0,
|
||||||
|
20.0,
|
||||||
|
50,
|
||||||
|
3.0,
|
||||||
|
0,
|
||||||
|
1013.25,
|
||||||
|
980.0,
|
||||||
|
15.0,
|
||||||
|
30.0,
|
||||||
|
30.0,
|
||||||
|
WeatherInterpretation.getKnownWeatherCodes().random()
|
||||||
|
),
|
||||||
|
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
||||||
|
|
||||||
|
OpenMeteoForecastData(
|
||||||
|
forecastTimes,
|
||||||
|
forecastTemperatures,
|
||||||
|
forecastPrecipitationPropability,
|
||||||
|
forecastPrecipitation,
|
||||||
|
forecastWeatherCodes,
|
||||||
|
forecastWindSpeed,
|
||||||
|
forecastWindDirection,
|
||||||
|
forecastWindGusts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val distancePerHour =
|
||||||
|
settingsAndProfile?.settings?.getForecastMetersPerHour(settingsAndProfile.isImperial)
|
||||||
|
?.toDouble() ?: 0.0
|
||||||
|
val gpsCoords =
|
||||||
|
GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour)
|
||||||
|
|
||||||
|
WeatherDataResponse(weatherData, gpsCoords)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
emit(
|
||||||
|
StreamData(
|
||||||
|
data,
|
||||||
|
SettingsAndProfile(
|
||||||
|
HeadwindSettings(),
|
||||||
|
settingsAndProfile?.isImperial == true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
delay(5_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Starting weather forecast view with $emitter")
|
||||||
|
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
||||||
|
awaitCancellation()
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseBitmap = BitmapFactory.decodeResource(
|
||||||
|
context.resources,
|
||||||
|
R.drawable.arrow_0
|
||||||
|
)
|
||||||
|
|
||||||
|
val settingsAndProfileStream = context.streamSettings(karooSystem).combine(karooSystem.streamUserProfile()) { settings, userProfile ->
|
||||||
|
SettingsAndProfile(settings = settings, isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataFlow = if (config.preview){
|
||||||
|
previewFlow(settingsAndProfileStream)
|
||||||
|
} else {
|
||||||
|
combine(
|
||||||
|
context.streamCurrentWeatherData(),
|
||||||
|
settingsAndProfileStream,
|
||||||
|
context.streamWidgetSettings(),
|
||||||
|
karooSystem.getHeadingFlow(context),
|
||||||
|
karooSystem.streamUpcomingRoute()
|
||||||
|
) { weatherData, settings, widgetSettings, heading, upcomingRoute ->
|
||||||
|
StreamData(
|
||||||
|
data = weatherData,
|
||||||
|
settings = settings,
|
||||||
|
widgetSettings = widgetSettings,
|
||||||
|
headingResponse = heading,
|
||||||
|
upcomingRoute = upcomingRoute
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
emitter.onNext(ShowCustomStreamState("", null))
|
||||||
|
|
||||||
|
dataFlow.collect { (allData, settingsAndProfile, widgetSettings, userProfile, headingResponse, upcomingRoute) ->
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
|
||||||
|
|
||||||
|
if (allData == null){
|
||||||
|
emitter.updateView(
|
||||||
|
getErrorWidget(
|
||||||
|
glance,
|
||||||
|
context,
|
||||||
|
settingsAndProfile.settings,
|
||||||
|
headingResponse
|
||||||
|
).remoteViews)
|
||||||
|
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
|
var modifier = GlanceModifier.fillMaxSize()
|
||||||
|
|
||||||
|
if (!config.preview) modifier = modifier.clickable(onClick = actionRunCallback<CycleHoursAction>())
|
||||||
|
|
||||||
|
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.Start) {
|
||||||
|
val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0
|
||||||
|
val positionOffset = if (allData.size == 1) 0 else hourOffset
|
||||||
|
|
||||||
|
var previousDate: String? = let {
|
||||||
|
val unixTime =
|
||||||
|
allData.getOrNull(positionOffset)?.data?.forecastData?.time?.getOrNull(
|
||||||
|
hourOffset
|
||||||
|
)
|
||||||
|
val formattedDate = unixTime?.let {
|
||||||
|
getShortDateFormatter().format(
|
||||||
|
Instant.ofEpochSecond(unixTime)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
for (baseIndex in hourOffset..hourOffset + 2) {
|
||||||
|
val positionIndex = if (allData.size == 1) 0 else baseIndex
|
||||||
|
|
||||||
|
if (allData.getOrNull(positionIndex) == null) break
|
||||||
|
if (baseIndex >= (allData.getOrNull(positionOffset)?.data?.forecastData?.weatherCode?.size
|
||||||
|
?: 0)
|
||||||
|
) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = allData.getOrNull(positionIndex)?.data
|
||||||
|
val distanceAlongRoute =
|
||||||
|
allData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
|
||||||
|
val position =
|
||||||
|
allData.getOrNull(positionIndex)?.requestedPosition?.let {
|
||||||
|
"${
|
||||||
|
(it.distanceAlongRoute?.div(1000.0))?.toInt()
|
||||||
|
} at ${it.lat}, ${it.lon}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseIndex > hourOffset) {
|
||||||
|
Spacer(
|
||||||
|
modifier = GlanceModifier.fillMaxHeight().background(
|
||||||
|
ColorProvider(Color.Black, Color.White)
|
||||||
|
).width(1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
KarooHeadwindExtension.TAG,
|
||||||
|
"Distance along route ${positionIndex}: $position"
|
||||||
|
)
|
||||||
|
|
||||||
|
val distanceFromCurrent =
|
||||||
|
upcomingRoute?.distanceAlongRoute?.let { currentDistanceAlongRoute ->
|
||||||
|
distanceAlongRoute?.minus(currentDistanceAlongRoute)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isCurrent = baseIndex == 0 && positionIndex == 0
|
||||||
|
|
||||||
|
if (isCurrent && data?.current != null) {
|
||||||
|
val interpretation =
|
||||||
|
WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
||||||
|
val unixTime = data.current.time
|
||||||
|
val formattedTime =
|
||||||
|
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||||
|
val formattedDate =
|
||||||
|
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||||
|
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||||
|
|
||||||
|
RenderWidget(
|
||||||
|
arrowBitmap = baseBitmap,
|
||||||
|
current = interpretation,
|
||||||
|
windBearing = data.current.windDirection.roundToInt(),
|
||||||
|
windSpeed = data.current.windSpeed.roundToInt(),
|
||||||
|
windGusts = data.current.windGusts.roundToInt(),
|
||||||
|
precipitation = data.current.precipitation,
|
||||||
|
precipitationProbability = null,
|
||||||
|
temperature = data.current.temperature.roundToInt(),
|
||||||
|
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
|
timeLabel = formattedTime,
|
||||||
|
dateLabel = if (hasNewDate) formattedDate else null,
|
||||||
|
distance = null,
|
||||||
|
isImperial = userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
|
)
|
||||||
|
|
||||||
|
previousDate = formattedDate
|
||||||
|
} else {
|
||||||
|
val interpretation = WeatherInterpretation.fromWeatherCode(
|
||||||
|
data?.forecastData?.weatherCode?.get(baseIndex) ?: 0
|
||||||
|
)
|
||||||
|
val unixTime = data?.forecastData?.time?.get(baseIndex) ?: 0
|
||||||
|
val formattedTime =
|
||||||
|
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||||
|
val formattedDate =
|
||||||
|
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||||
|
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||||
|
|
||||||
|
RenderWidget(
|
||||||
|
arrowBitmap = baseBitmap,
|
||||||
|
current = interpretation,
|
||||||
|
windBearing = data?.forecastData?.windDirection?.get(baseIndex)
|
||||||
|
?.roundToInt() ?: 0,
|
||||||
|
windSpeed = data?.forecastData?.windSpeed?.get(baseIndex)
|
||||||
|
?.roundToInt() ?: 0,
|
||||||
|
windGusts = data?.forecastData?.windGusts?.get(baseIndex)
|
||||||
|
?.roundToInt() ?: 0,
|
||||||
|
precipitation = data?.forecastData?.precipitation?.get(baseIndex)
|
||||||
|
?: 0.0,
|
||||||
|
precipitationProbability = data?.forecastData?.precipitationProbability?.get(
|
||||||
|
baseIndex
|
||||||
|
) ?: 0,
|
||||||
|
temperature = data?.forecastData?.temperature?.get(baseIndex)
|
||||||
|
?.roundToInt() ?: 0,
|
||||||
|
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
|
timeLabel = formattedTime,
|
||||||
|
dateLabel = if (hasNewDate) formattedDate else null,
|
||||||
|
distance = if (settingsAndProfile.settings.showDistanceInForecast) distanceFromCurrent else null,
|
||||||
|
isImperial = settingsAndProfile.isImperial
|
||||||
|
)
|
||||||
|
|
||||||
|
previousDate = formattedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.updateView(result.remoteViews)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitter.setCancellable {
|
||||||
|
Log.d(
|
||||||
|
KarooHeadwindExtension.TAG,
|
||||||
|
"Stopping headwind weather forecast view with $emitter"
|
||||||
|
)
|
||||||
|
configJob.cancel()
|
||||||
|
viewJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.ColorFilter
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.color.ColorProvider
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.ContentScale
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.fillMaxHeight
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
|
import androidx.glance.layout.padding
|
||||||
|
import androidx.glance.layout.width
|
||||||
|
import androidx.glance.layout.wrapContentWidth
|
||||||
|
import androidx.glance.text.FontFamily
|
||||||
|
import androidx.glance.text.FontWeight
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextAlign
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GraphicalForecast(
|
||||||
|
current: WeatherInterpretation,
|
||||||
|
distance: Double? = null,
|
||||||
|
timeLabel: String? = null,
|
||||||
|
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
||||||
|
isImperial: Boolean?
|
||||||
|
) {
|
||||||
|
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
||||||
|
Row(modifier = GlanceModifier.defaultWeight().wrapContentWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Image(
|
||||||
|
modifier = GlanceModifier.defaultWeight().wrapContentWidth().padding(1.dp),
|
||||||
|
provider = ImageProvider(getWeatherIcon(current)),
|
||||||
|
contentDescription = "Current weather information",
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance != null && isImperial != null){
|
||||||
|
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||||
|
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||||
|
val text = if(distanceInUserUnit > 0){
|
||||||
|
"In $label"
|
||||||
|
} else {
|
||||||
|
"$label ago"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceInUserUnit != 0){
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(
|
||||||
|
color = ColorProvider(Color.Black, Color.White),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeLabel != null){
|
||||||
|
Text(
|
||||||
|
text = timeLabel,
|
||||||
|
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GraphicalForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "graphicalForecast") {
|
||||||
|
@Composable
|
||||||
|
override fun RenderWidget(
|
||||||
|
arrowBitmap: Bitmap,
|
||||||
|
current: WeatherInterpretation,
|
||||||
|
windBearing: Int,
|
||||||
|
windSpeed: Int,
|
||||||
|
windGusts: Int,
|
||||||
|
precipitation: Double,
|
||||||
|
precipitationProbability: Int?,
|
||||||
|
temperature: Int,
|
||||||
|
temperatureUnit: TemperatureUnit,
|
||||||
|
timeLabel: String,
|
||||||
|
dateLabel: String?,
|
||||||
|
distance: Double?,
|
||||||
|
isImperial: Boolean
|
||||||
|
) {
|
||||||
|
GraphicalForecast(
|
||||||
|
current = current,
|
||||||
|
distance = distance,
|
||||||
|
timeLabel = timeLabel,
|
||||||
|
isImperial = isImperial
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.color.ColorProvider
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.fillMaxHeight
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
|
import androidx.glance.layout.padding
|
||||||
|
import androidx.glance.layout.width
|
||||||
|
import androidx.glance.text.FontFamily
|
||||||
|
import androidx.glance.text.FontWeight
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextAlign
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrecipitationForecast(
|
||||||
|
precipitation: Int,
|
||||||
|
precipitationProbability: Int?,
|
||||||
|
distance: Double? = null,
|
||||||
|
timeLabel: String? = null,
|
||||||
|
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
||||||
|
isImperial: Boolean?
|
||||||
|
) {
|
||||||
|
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
||||||
|
Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
val precipitationProbabilityText = if (precipitationProbability != null) "${precipitationProbability}% " else ""
|
||||||
|
val precipitationText = if (precipitation > 0) precipitation.toString() else ""
|
||||||
|
Text(
|
||||||
|
text = "${precipitationProbabilityText}${precipitationText}",
|
||||||
|
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(24f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance != null && isImperial != null){
|
||||||
|
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||||
|
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||||
|
val text = if(distanceInUserUnit > 0){
|
||||||
|
"In $label"
|
||||||
|
} else {
|
||||||
|
"$label ago"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceInUserUnit != 0){
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(
|
||||||
|
color = ColorProvider(Color.Black, Color.White),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeLabel != null){
|
||||||
|
Text(
|
||||||
|
text = timeLabel,
|
||||||
|
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrecipitationForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "precipitationForecast") {
|
||||||
|
@Composable
|
||||||
|
override fun RenderWidget(
|
||||||
|
arrowBitmap: Bitmap,
|
||||||
|
current: WeatherInterpretation,
|
||||||
|
windBearing: Int,
|
||||||
|
windSpeed: Int,
|
||||||
|
windGusts: Int,
|
||||||
|
precipitation: Double,
|
||||||
|
precipitationProbability: Int?,
|
||||||
|
temperature: Int,
|
||||||
|
temperatureUnit: TemperatureUnit,
|
||||||
|
timeLabel: String,
|
||||||
|
dateLabel: String?,
|
||||||
|
distance: Double?,
|
||||||
|
isImperial: Boolean
|
||||||
|
) {
|
||||||
|
PrecipitationForecast(
|
||||||
|
precipitation = precipitation.roundToInt().coerceAtLeast(0),
|
||||||
|
precipitationProbability = precipitationProbability,
|
||||||
|
distance = distance,
|
||||||
|
timeLabel = timeLabel,
|
||||||
|
isImperial = isImperial
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.color.ColorProvider
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.fillMaxHeight
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
|
import androidx.glance.layout.padding
|
||||||
|
import androidx.glance.layout.width
|
||||||
|
import androidx.glance.text.FontFamily
|
||||||
|
import androidx.glance.text.FontWeight
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextAlign
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TemperatureForecast(
|
||||||
|
temperature: Int,
|
||||||
|
temperatureUnit: TemperatureUnit,
|
||||||
|
distance: Double? = null,
|
||||||
|
timeLabel: String? = null,
|
||||||
|
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
||||||
|
isImperial: Boolean?
|
||||||
|
) {
|
||||||
|
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
||||||
|
Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "${temperature}${temperatureUnit.unitDisplay}",
|
||||||
|
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(28f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance != null && isImperial != null){
|
||||||
|
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||||
|
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||||
|
val text = if(distanceInUserUnit > 0){
|
||||||
|
"In $label"
|
||||||
|
} else {
|
||||||
|
"$label ago"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceInUserUnit != 0){
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(
|
||||||
|
color = ColorProvider(Color.Black, Color.White),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeLabel != null){
|
||||||
|
Text(
|
||||||
|
text = timeLabel,
|
||||||
|
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TemperatureForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "temperatureForecast") {
|
||||||
|
@Composable
|
||||||
|
override fun RenderWidget(
|
||||||
|
arrowBitmap: Bitmap,
|
||||||
|
current: WeatherInterpretation,
|
||||||
|
windBearing: Int,
|
||||||
|
windSpeed: Int,
|
||||||
|
windGusts: Int,
|
||||||
|
precipitation: Double,
|
||||||
|
precipitationProbability: Int?,
|
||||||
|
temperature: Int,
|
||||||
|
temperatureUnit: TemperatureUnit,
|
||||||
|
timeLabel: String,
|
||||||
|
dateLabel: String?,
|
||||||
|
distance: Double?,
|
||||||
|
isImperial: Boolean
|
||||||
|
) {
|
||||||
|
TemperatureForecast(
|
||||||
|
temperature = temperature,
|
||||||
|
temperatureUnit = temperatureUnit,
|
||||||
|
distance = distance,
|
||||||
|
timeLabel = timeLabel,
|
||||||
|
isImperial = isImperial
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,262 +1,43 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import androidx.compose.runtime.Composable
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.glance.GlanceModifier
|
|
||||||
import androidx.glance.action.clickable
|
|
||||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
|
||||||
import androidx.glance.appwidget.GlanceRemoteViews
|
|
||||||
import androidx.glance.appwidget.action.actionRunCallback
|
|
||||||
import androidx.glance.background
|
|
||||||
import androidx.glance.color.ColorProvider
|
|
||||||
import androidx.glance.layout.Alignment
|
|
||||||
import androidx.glance.layout.Row
|
|
||||||
import androidx.glance.layout.Spacer
|
|
||||||
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.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.UpcomingRoute
|
|
||||||
import de.timklge.karooheadwind.WeatherDataResponse
|
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
import de.timklge.karooheadwind.getHeadingFlow
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
|
||||||
import de.timklge.karooheadwind.streamSettings
|
|
||||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
|
||||||
import de.timklge.karooheadwind.streamUserProfile
|
|
||||||
import de.timklge.karooheadwind.streamWidgetSettings
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
|
||||||
import io.hammerhead.karooext.internal.ViewEmitter
|
|
||||||
import io.hammerhead.karooext.models.ShowCustomStreamState
|
|
||||||
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
|
||||||
import io.hammerhead.karooext.models.UserProfile
|
|
||||||
import io.hammerhead.karooext.models.ViewConfig
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.format.FormatStyle
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
class WeatherForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "weatherForecast") {
|
||||||
class WeatherForecastDataType(
|
@Composable
|
||||||
private val karooSystem: KarooSystemService,
|
override fun RenderWidget(
|
||||||
private val applicationContext: Context
|
arrowBitmap: Bitmap,
|
||||||
) : DataTypeImpl("karoo-headwind", "weatherForecast") {
|
current: WeatherInterpretation,
|
||||||
private val glance = GlanceRemoteViews()
|
windBearing: Int,
|
||||||
|
windSpeed: Int,
|
||||||
companion object {
|
windGusts: Int,
|
||||||
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())
|
precipitation: Double,
|
||||||
}
|
precipitationProbability: Int?,
|
||||||
|
temperature: Int,
|
||||||
data class StreamData(val data: List<WeatherDataResponse>?, val settings: SettingsAndProfile,
|
temperatureUnit: TemperatureUnit,
|
||||||
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null,
|
timeLabel: String,
|
||||||
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null)
|
dateLabel: String?,
|
||||||
|
distance: Double?,
|
||||||
data class SettingsAndProfile(val settings: HeadwindSettings, val isImperial: Boolean)
|
isImperial: Boolean
|
||||||
|
) {
|
||||||
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> = flow {
|
Weather(
|
||||||
val settingsAndProfile = settingsAndProfileStream.firstOrNull()
|
arrowBitmap = arrowBitmap,
|
||||||
|
current = current,
|
||||||
while (true){
|
windBearing = windBearing,
|
||||||
val data = (0..<10).map { index ->
|
windSpeed = windSpeed,
|
||||||
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
|
windGusts = windGusts,
|
||||||
val forecastTimes = (0..<12).map { timeAtFullHour + it * 60 * 60 }
|
precipitation = precipitation,
|
||||||
val forecastTemperatures = (0..<12).map { 20.0 + (-20..20).random() }
|
precipitationProbability = precipitationProbability,
|
||||||
val forecastPrecipitationPropability = (0..<12).map { (0..100).random() }
|
temperature = temperature,
|
||||||
val forecastPrecipitation = (0..<12).map { 0.0 + (0..10).random() }
|
temperatureUnit = temperatureUnit,
|
||||||
val forecastWeatherCodes = (0..<12).map { WeatherInterpretation.getKnownWeatherCodes().random() }
|
timeLabel = timeLabel,
|
||||||
val forecastWindSpeed = (0..<12).map { 0.0 + (0..10).random() }
|
dateLabel = dateLabel,
|
||||||
val forecastWindDirection = (0..<12).map { 0.0 + (0..360).random() }
|
distance = distance,
|
||||||
val forecastWindGusts = (0..<12).map { 0.0 + (0..10).random() }
|
isImperial = isImperial
|
||||||
val weatherData = OpenMeteoCurrentWeatherResponse(
|
|
||||||
OpenMeteoData(Instant.now().epochSecond, 0, 20.0, 50, 3.0, 0, 1013.25, 980.0, 15.0, 30.0, 30.0, WeatherInterpretation.getKnownWeatherCodes().random()),
|
|
||||||
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
|
||||||
|
|
||||||
OpenMeteoForecastData(forecastTimes, forecastTemperatures, forecastPrecipitationPropability,
|
|
||||||
forecastPrecipitation, forecastWeatherCodes, forecastWindSpeed, forecastWindDirection,
|
|
||||||
forecastWindGusts)
|
|
||||||
)
|
|
||||||
|
|
||||||
val distancePerHour = settingsAndProfile?.settings?.getForecastMetersPerHour(settingsAndProfile.isImperial)?.toDouble() ?: 0.0
|
|
||||||
val gpsCoords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour)
|
|
||||||
|
|
||||||
WeatherDataResponse(weatherData, gpsCoords)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
emit(
|
|
||||||
StreamData(data, SettingsAndProfile(HeadwindSettings(), settingsAndProfile?.isImperial == true))
|
|
||||||
)
|
|
||||||
|
|
||||||
delay(5_000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Starting weather forecast view with $emitter")
|
|
||||||
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
|
||||||
awaitCancellation()
|
|
||||||
}
|
|
||||||
|
|
||||||
val baseBitmap = BitmapFactory.decodeResource(
|
|
||||||
context.resources,
|
|
||||||
de.timklge.karooheadwind.R.drawable.arrow_0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val settingsAndProfileStream = context.streamSettings(karooSystem).combine(karooSystem.streamUserProfile()) { settings, userProfile ->
|
|
||||||
SettingsAndProfile(settings = settings, isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataFlow = if (config.preview){
|
|
||||||
previewFlow(settingsAndProfileStream)
|
|
||||||
} else {
|
|
||||||
combine(context.streamCurrentWeatherData(),
|
|
||||||
settingsAndProfileStream,
|
|
||||||
context.streamWidgetSettings(),
|
|
||||||
karooSystem.getHeadingFlow(context),
|
|
||||||
karooSystem.streamUpcomingRoute()) { weatherData, settings, widgetSettings, heading, upcomingRoute ->
|
|
||||||
StreamData(data = weatherData, settings = settings, widgetSettings = widgetSettings, headingResponse = heading, upcomingRoute = upcomingRoute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
emitter.onNext(ShowCustomStreamState("", null))
|
|
||||||
|
|
||||||
dataFlow.collect { (allData, settingsAndProfile, widgetSettings, userProfile, headingResponse, upcomingRoute) ->
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
|
|
||||||
|
|
||||||
if (allData == null){
|
|
||||||
emitter.updateView(getErrorWidget(glance, context, settingsAndProfile.settings, headingResponse).remoteViews)
|
|
||||||
|
|
||||||
return@collect
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = glance.compose(context, DpSize.Unspecified) {
|
|
||||||
var modifier = GlanceModifier.fillMaxSize()
|
|
||||||
|
|
||||||
if (!config.preview) modifier = modifier.clickable(onClick = actionRunCallback<CycleHoursAction>())
|
|
||||||
|
|
||||||
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.Start) {
|
|
||||||
val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0
|
|
||||||
val positionOffset = if (allData.size == 1) 0 else hourOffset
|
|
||||||
|
|
||||||
var previousDate: String? = let {
|
|
||||||
val unixTime = allData.getOrNull(positionOffset)?.data?.forecastData?.time?.getOrNull(hourOffset)
|
|
||||||
val formattedDate = unixTime?.let { getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) }
|
|
||||||
|
|
||||||
formattedDate
|
|
||||||
}
|
|
||||||
|
|
||||||
for (baseIndex in hourOffset..hourOffset + 2){
|
|
||||||
val positionIndex = if (allData.size == 1) 0 else baseIndex
|
|
||||||
|
|
||||||
if (allData.getOrNull(positionIndex) == null) break
|
|
||||||
if (baseIndex >= (allData.getOrNull(positionOffset)?.data?.forecastData?.weatherCode?.size ?: 0)) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
val data = allData.getOrNull(positionIndex)?.data
|
|
||||||
val distanceAlongRoute = allData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
|
|
||||||
val position = allData.getOrNull(positionIndex)?.requestedPosition?.let { "${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}" }
|
|
||||||
|
|
||||||
if (baseIndex > hourOffset) {
|
|
||||||
Spacer(
|
|
||||||
modifier = GlanceModifier.fillMaxHeight().background(
|
|
||||||
ColorProvider(Color.Black, Color.White)
|
|
||||||
).width(1.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Distance along route ${positionIndex}: $position")
|
|
||||||
|
|
||||||
val distanceFromCurrent = upcomingRoute?.distanceAlongRoute?.let { currentDistanceAlongRoute ->
|
|
||||||
distanceAlongRoute?.minus(currentDistanceAlongRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isCurrent = baseIndex == 0 && positionIndex == 0
|
|
||||||
|
|
||||||
if (isCurrent && data?.current != null){
|
|
||||||
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
|
||||||
val unixTime = data.current.time
|
|
||||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
|
||||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
|
||||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
|
||||||
|
|
||||||
Weather(
|
|
||||||
baseBitmap,
|
|
||||||
current = interpretation,
|
|
||||||
windBearing = data.current.windDirection.roundToInt(),
|
|
||||||
windSpeed = data.current.windSpeed.roundToInt(),
|
|
||||||
windGusts = data.current.windGusts.roundToInt(),
|
|
||||||
precipitation = data.current.precipitation,
|
|
||||||
precipitationProbability = null,
|
|
||||||
temperature = data.current.temperature.roundToInt(),
|
|
||||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
|
||||||
timeLabel = formattedTime,
|
|
||||||
dateLabel = if (hasNewDate) formattedDate else null,
|
|
||||||
isImperial = userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
|
||||||
)
|
|
||||||
|
|
||||||
previousDate = formattedDate
|
|
||||||
} else {
|
|
||||||
val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(baseIndex) ?: 0)
|
|
||||||
val unixTime = data?.forecastData?.time?.get(baseIndex) ?: 0
|
|
||||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
|
||||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
|
||||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
|
||||||
|
|
||||||
Weather(
|
|
||||||
baseBitmap,
|
|
||||||
current = interpretation,
|
|
||||||
windBearing = data?.forecastData?.windDirection?.get(baseIndex)?.roundToInt() ?: 0,
|
|
||||||
windSpeed = data?.forecastData?.windSpeed?.get(baseIndex)?.roundToInt() ?: 0,
|
|
||||||
windGusts = data?.forecastData?.windGusts?.get(baseIndex)?.roundToInt() ?: 0,
|
|
||||||
precipitation = data?.forecastData?.precipitation?.get(baseIndex) ?: 0.0,
|
|
||||||
precipitationProbability = data?.forecastData?.precipitationProbability?.get(baseIndex) ?: 0,
|
|
||||||
temperature = data?.forecastData?.temperature?.get(baseIndex)?.roundToInt() ?: 0,
|
|
||||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
|
||||||
timeLabel = formattedTime,
|
|
||||||
dateLabel = if (hasNewDate) formattedDate else null,
|
|
||||||
distance = distanceFromCurrent,
|
|
||||||
isImperial = settingsAndProfile.isImperial
|
|
||||||
)
|
|
||||||
|
|
||||||
previousDate = formattedDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitter.updateView(result.remoteViews)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emitter.setCancellable {
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Stopping headwind weather forecast view with $emitter")
|
|
||||||
configJob.cancel()
|
|
||||||
viewJob.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ fun getWeatherIcon(interpretation: WeatherInterpretation): Int {
|
|||||||
@OptIn(ExperimentalGlancePreviewApi::class)
|
@OptIn(ExperimentalGlancePreviewApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Weather(
|
fun Weather(
|
||||||
baseBitmap: Bitmap,
|
arrowBitmap: Bitmap,
|
||||||
current: WeatherInterpretation,
|
current: WeatherInterpretation,
|
||||||
windBearing: Int,
|
windBearing: Int,
|
||||||
windSpeed: Int,
|
windSpeed: Int,
|
||||||
@ -172,7 +172,7 @@ fun Weather(
|
|||||||
|
|
||||||
Image(
|
Image(
|
||||||
modifier = if (singleDisplay) GlanceModifier.height(20.dp).width(16.dp) else GlanceModifier.height(16.dp).width(12.dp).padding(1.dp),
|
modifier = if (singleDisplay) GlanceModifier.height(20.dp).width(16.dp) else GlanceModifier.height(16.dp).width(12.dp).padding(1.dp),
|
||||||
provider = ImageProvider(getArrowBitmapByBearing(baseBitmap, windBearing + 180)),
|
provider = ImageProvider(getArrowBitmapByBearing(arrowBitmap, windBearing + 180)),
|
||||||
contentDescription = "Current wind direction",
|
contentDescription = "Current wind direction",
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.ColorFilter
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.color.ColorProvider
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.ContentScale
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.fillMaxHeight
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
|
import androidx.glance.layout.height
|
||||||
|
import androidx.glance.layout.padding
|
||||||
|
import androidx.glance.layout.width
|
||||||
|
import androidx.glance.text.FontFamily
|
||||||
|
import androidx.glance.text.FontWeight
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextAlign
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WindForecast(
|
||||||
|
arrowBitmap: Bitmap,
|
||||||
|
windBearing: Int,
|
||||||
|
windSpeed: Int,
|
||||||
|
gustSpeed: Int,
|
||||||
|
distance: Double? = null,
|
||||||
|
timeLabel: String? = null,
|
||||||
|
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
||||||
|
isImperial: Boolean?
|
||||||
|
) {
|
||||||
|
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
||||||
|
Image(
|
||||||
|
modifier = GlanceModifier.defaultWeight().fillMaxWidth(),
|
||||||
|
provider = ImageProvider(getArrowBitmapByBearing(arrowBitmap, windBearing + 180)),
|
||||||
|
contentDescription = "Current wind direction",
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${windSpeed}-${gustSpeed}",
|
||||||
|
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (distance != null && isImperial != null){
|
||||||
|
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||||
|
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||||
|
val text = if(distanceInUserUnit > 0){
|
||||||
|
"In $label"
|
||||||
|
} else {
|
||||||
|
"$label ago"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceInUserUnit != 0){
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(
|
||||||
|
color = ColorProvider(Color.Black, Color.White),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeLabel != null){
|
||||||
|
Text(
|
||||||
|
text = timeLabel,
|
||||||
|
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "windForecast") {
|
||||||
|
@Composable
|
||||||
|
override fun RenderWidget(
|
||||||
|
arrowBitmap: Bitmap,
|
||||||
|
current: WeatherInterpretation,
|
||||||
|
windBearing: Int,
|
||||||
|
windSpeed: Int,
|
||||||
|
windGusts: Int,
|
||||||
|
precipitation: Double,
|
||||||
|
precipitationProbability: Int?,
|
||||||
|
temperature: Int,
|
||||||
|
temperatureUnit: TemperatureUnit,
|
||||||
|
timeLabel: String,
|
||||||
|
dateLabel: String?,
|
||||||
|
distance: Double?,
|
||||||
|
isImperial: Boolean
|
||||||
|
) {
|
||||||
|
WindForecast(
|
||||||
|
arrowBitmap = arrowBitmap,
|
||||||
|
windBearing = windBearing,
|
||||||
|
windSpeed = windSpeed,
|
||||||
|
gustSpeed = windGusts,
|
||||||
|
distance = distance,
|
||||||
|
timeLabel = timeLabel,
|
||||||
|
isImperial = isImperial
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,13 +4,16 @@ import android.util.Log
|
|||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@ -21,6 +24,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
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.text.input.KeyboardType
|
||||||
@ -62,6 +66,7 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
|||||||
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_3) }
|
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_3) }
|
||||||
var forecastKmPerHour by remember { mutableStateOf("20") }
|
var forecastKmPerHour by remember { mutableStateOf("20") }
|
||||||
var forecastMilesPerHour by remember { mutableStateOf("12") }
|
var forecastMilesPerHour by remember { mutableStateOf("12") }
|
||||||
|
var showDistanceInForecast by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
|
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
@ -73,6 +78,7 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
|||||||
selectedRoundLocationSetting = settings.roundLocationTo
|
selectedRoundLocationSetting = settings.roundLocationTo
|
||||||
forecastKmPerHour = settings.forecastedKmPerHour.toString()
|
forecastKmPerHour = settings.forecastedKmPerHour.toString()
|
||||||
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
|
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
|
||||||
|
showDistanceInForecast = settings.showDistanceInForecast
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +104,8 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
|||||||
windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
|
windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
|
||||||
roundLocationTo = selectedRoundLocationSetting,
|
roundLocationTo = selectedRoundLocationSetting,
|
||||||
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
|
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
|
||||||
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20
|
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20,
|
||||||
|
showDistanceInForecast = showDistanceInForecast
|
||||||
)
|
)
|
||||||
|
|
||||||
saveSettings(ctx, newSettings)
|
saveSettings(ctx, newSettings)
|
||||||
@ -205,6 +212,12 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Switch(checked = showDistanceInForecast, onCheckedChange = { showDistanceInForecast = it})
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Text("Show Distance in Forecast")
|
||||||
|
}
|
||||||
|
|
||||||
if (!karooConnected) {
|
if (!karooConnected) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(5.dp),
|
modifier = Modifier.padding(5.dp),
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import de.timklge.karooheadwind.KarooHeadwindExtension
|
|||||||
import de.timklge.karooheadwind.R
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
|
import de.timklge.karooheadwind.datatypes.ForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
|
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.getShortDateFormatter
|
import de.timklge.karooheadwind.datatypes.getShortDateFormatter
|
||||||
@ -200,7 +201,7 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
|||||||
|
|
||||||
val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(index) ?: 0)
|
val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(index) ?: 0)
|
||||||
val unixTime = data?.forecastData?.time?.get(index) ?: 0
|
val unixTime = data?.forecastData?.time?.get(index) ?: 0
|
||||||
val formattedForecastTime = WeatherForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
val formattedForecastTime = ForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||||
val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||||
|
|
||||||
WeatherWidget(
|
WeatherWidget(
|
||||||
|
|||||||
@ -25,10 +25,18 @@
|
|||||||
<string name="weather_description">Current weather conditions</string>
|
<string name="weather_description">Current weather conditions</string>
|
||||||
<string name="weather_forecast">Weather Forecast</string>
|
<string name="weather_forecast">Weather Forecast</string>
|
||||||
<string name="weather_forecast_description">Current hourly weather forecast</string>
|
<string name="weather_forecast_description">Current hourly weather forecast</string>
|
||||||
|
<string name="temperature_forecast">Temperature Forecast</string>
|
||||||
|
<string name="temperature_forecast_description">Current hourly temperature forecast</string>
|
||||||
|
<string name="wind_forecast">Wind Forecast</string>
|
||||||
|
<string name="wind_forecast_description">Current hourly wind forecast</string>
|
||||||
|
<string name="precipitation_forecast">Precipitation Forecast</string>
|
||||||
|
<string name="precipitation_forecast_description">Current hourly precipitation forecast</string>
|
||||||
<string name="headwind_speed">Headwind speed</string>
|
<string name="headwind_speed">Headwind speed</string>
|
||||||
<string name="headwind_speed_description">Current headwind speed</string>
|
<string name="headwind_speed_description">Current headwind speed</string>
|
||||||
<string name="temperature">Temperature</string>
|
<string name="temperature">Temperature</string>
|
||||||
<string name="temperature_description">Current temperature in configured unit</string>
|
<string name="temperature_description">Current temperature in configured unit</string>
|
||||||
<string name="userwind_speed_description">Current headwind or wind speed based on user setting</string>
|
<string name="userwind_speed_description">Current headwind or wind speed based on user setting</string>
|
||||||
<string name="userwind_speed">Set wind speed</string>
|
<string name="userwind_speed">Set wind speed</string>
|
||||||
|
<string name="graphical_forecast">Graphical Forecast</string>
|
||||||
|
<string name="graphical_forecast_description">Current graphical weather forecast</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -32,6 +32,34 @@
|
|||||||
icon="@drawable/wind"
|
icon="@drawable/wind"
|
||||||
typeId="weatherForecast" />
|
typeId="weatherForecast" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/temperature_forecast_description"
|
||||||
|
displayName="@string/temperature_forecast"
|
||||||
|
graphical="true"
|
||||||
|
icon="@drawable/wind"
|
||||||
|
typeId="temperatureForecast" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/precipitation_forecast_description"
|
||||||
|
displayName="@string/precipitation_forecast"
|
||||||
|
graphical="true"
|
||||||
|
icon="@drawable/wind"
|
||||||
|
typeId="precipitationForecast" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/wind_forecast_description"
|
||||||
|
displayName="@string/wind_forecast"
|
||||||
|
graphical="true"
|
||||||
|
icon="@drawable/wind"
|
||||||
|
typeId="windForecast" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/graphical_forecast_description"
|
||||||
|
displayName="@string/graphical_forecast"
|
||||||
|
graphical="true"
|
||||||
|
icon="@drawable/wind"
|
||||||
|
typeId="graphicalForecast" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/headwind_speed_description"
|
description="@string/headwind_speed_description"
|
||||||
displayName="@string/headwind_speed"
|
displayName="@string/headwind_speed"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user