231 lines
11 KiB
Kotlin
231 lines
11 KiB
Kotlin
package de.timklge.karooheadwind.screens
|
|
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
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.rememberScrollState
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.Icons
|
|
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.Text
|
|
import androidx.compose.material3.TopAppBar
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.collectAsState
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.unit.dp
|
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
|
import de.timklge.karooheadwind.getGpsCoordinateFlow
|
|
import de.timklge.karooheadwind.saveSettings
|
|
import de.timklge.karooheadwind.streamSettings
|
|
import de.timklge.karooheadwind.streamStats
|
|
import io.hammerhead.karooext.KarooSystemService
|
|
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){
|
|
MILLIMETERS("mm", "Millimeters (mm)"),
|
|
INCH("inch", "Inch")
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
@Serializable
|
|
data class HeadwindSettings(
|
|
val windUnit: WindUnit = WindUnit.KILOMETERS_PER_HOUR,
|
|
val precipitationUnit: PrecipitationUnit = PrecipitationUnit.MILLIMETERS,
|
|
val welcomeDialogAccepted: Boolean = false,
|
|
val windDirectionIndicatorTextSetting: WindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.HEADWIND_SPEED,
|
|
){
|
|
companion object {
|
|
val defaultSettings = Json.encodeToString(HeadwindSettings())
|
|
}
|
|
}
|
|
|
|
@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() {
|
|
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 selectedPrecipitationUnit by remember { mutableStateOf(PrecipitationUnit.MILLIMETERS) }
|
|
var welcomeDialogVisible by remember { mutableStateOf(false) }
|
|
var selectedWindDirectionIndicatorTextSetting by remember { mutableStateOf(WindDirectionIndicatorTextSetting.HEADWIND_SPEED) }
|
|
|
|
val stats by ctx.streamStats().collectAsState(HeadwindStats())
|
|
val location by karooSystem.getGpsCoordinateFlow().collectAsState(initial = null)
|
|
|
|
var savedDialogVisible by remember { mutableStateOf(false) }
|
|
|
|
LaunchedEffect(Unit) {
|
|
ctx.streamSettings(karooSystem).collect { settings ->
|
|
selectedWindUnit = settings.windUnit
|
|
selectedPrecipitationUnit = settings.precipitationUnit
|
|
welcomeDialogVisible = !settings.welcomeDialogAccepted
|
|
selectedWindDirectionIndicatorTextSetting = settings.windDirectionIndicatorTextSetting
|
|
}
|
|
}
|
|
|
|
LaunchedEffect(Unit) {
|
|
karooSystem.connect { connected ->
|
|
karooConnected = connected
|
|
}
|
|
}
|
|
|
|
Column(modifier = Modifier
|
|
.fillMaxSize()
|
|
.background(MaterialTheme.colorScheme.background)) {
|
|
TopAppBar(title = { Text("Headwind") })
|
|
Column(modifier = Modifier
|
|
.padding(5.dp)
|
|
.verticalScroll(rememberScrollState())
|
|
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
|
|
|
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 precipitationUnitDropdownOptions = PrecipitationUnit.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
|
|
val precipitationUnitInitialSelection by remember(selectedPrecipitationUnit) {
|
|
mutableStateOf(precipitationUnitDropdownOptions.find { option -> option.id == selectedPrecipitationUnit.id }!!)
|
|
}
|
|
Dropdown(label = "Precipitation Unit", options = precipitationUnitDropdownOptions, selected = precipitationUnitInitialSelection) { selectedOption ->
|
|
selectedPrecipitationUnit = PrecipitationUnit.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 }!!
|
|
}
|
|
|
|
FilledTonalButton(modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(50.dp), onClick = {
|
|
val newSettings = HeadwindSettings(windUnit = selectedWindUnit, precipitationUnit = selectedPrecipitationUnit,
|
|
welcomeDialogAccepted = true, windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting)
|
|
|
|
coroutineScope.launch {
|
|
saveSettings(ctx, newSettings)
|
|
savedDialogVisible = true
|
|
}
|
|
}) {
|
|
Icon(Icons.Default.Done, contentDescription = "Save")
|
|
Spacer(modifier = Modifier.width(5.dp))
|
|
Text("Save")
|
|
}
|
|
|
|
if (!karooConnected){
|
|
Text(modifier = Modifier.padding(5.dp), text = "Could not read device status. Is your Karoo updated?")
|
|
}
|
|
|
|
val lastPosition = location?.let { l -> stats.lastSuccessfulWeatherPosition?.distanceTo(l) }
|
|
val lastPositionDistanceStr = lastPosition?.let { dist -> " (${dist.roundToInt()} km away)" } ?: ""
|
|
|
|
if (stats.failedWeatherRequest != null && (stats.lastSuccessfulWeatherRequest == null || stats.failedWeatherRequest!! > stats.lastSuccessfulWeatherRequest!!)){
|
|
val successfulTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalTime().truncatedTo(
|
|
ChronoUnit.SECONDS)
|
|
val lastTryTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalTime().truncatedTo(
|
|
ChronoUnit.SECONDS)
|
|
|
|
val successStr = if(lastPosition != null) " Last data received at ${successfulTime}${lastPositionDistanceStr}." else ""
|
|
Text(modifier = Modifier.padding(5.dp), text = "Failed to update weather data; last try at ${lastTryTime}.${successStr}")
|
|
} else if(stats.lastSuccessfulWeatherRequest != null){
|
|
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 ${localTime}${lastPositionDistanceStr}")
|
|
} else {
|
|
Text(modifier = Modifier.padding(5.dp), text = "No weather data received yet, waiting for GPS fix...")
|
|
}
|
|
}
|
|
}
|
|
|
|
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, precipitationUnit = selectedPrecipitationUnit, 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.")
|
|
}
|
|
}
|
|
)
|
|
}
|
|
} |