Enderthor 973bed718f
Support to OpenWeatherMap 3.0 (#62)
* Added openweathermap support

* Set default location rounding to 3 km

* Back from settings page navigates to weather page first

* Enlarge font in single date weather display (#55)

* Shorter date format

* Enlarge font size in single date weather widget and make clickable

* Fix off by one in route forecast

* Fix indentation lint

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Update readme (openweathermap)

* Update readme (openweathermap)

* Timgkle fixes

* Timgkle fixes

* Delete app/manifest.json

* Timgkle fixes

* timklge updates 20250308

* timklge updates 20250308
Default provider is Open_METEO

---------

Co-authored-by: Tim Kluge <timklge@gmail.com>
Co-authored-by: timklge <2026103+timklge@users.noreply.github.com>
2025-03-23 21:54:20 +01:00

292 lines
12 KiB
Kotlin

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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.Switch
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.Alignment
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
import androidx.compose.material3.MaterialTheme
import de.timklge.karooheadwind.WeatherDataProvider
@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_3) }
var forecastKmPerHour by remember { mutableStateOf("20") }
var forecastMilesPerHour by remember { mutableStateOf("12") }
var showDistanceInForecast by remember { mutableStateOf(true) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
var selectedWeatherProvider by remember { mutableStateOf(WeatherDataProvider.OPEN_METEO) }
var openWeatherMapApiKey by remember { mutableStateOf("") }
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()
showDistanceInForecast = settings.showDistanceInForecast
selectedWeatherProvider = settings.weatherProvider
openWeatherMapApiKey = settings.openWeatherMapApiKey
}
}
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,
showDistanceInForecast = showDistanceInForecast,
weatherProvider = selectedWeatherProvider,
openWeatherMapApiKey = openWeatherMapApiKey
)
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
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = showDistanceInForecast, onCheckedChange = { showDistanceInForecast = it})
Spacer(modifier = Modifier.width(10.dp))
Text("Show Distance in Forecast")
}
if (!karooConnected) {
Text(
modifier = Modifier.padding(5.dp),
text = "Could not read device status. Is your Karoo updated?"
)
}
WeatherProviderSection(
selectedProvider = selectedWeatherProvider,
apiKey = openWeatherMapApiKey,
onProviderChanged = { selectedWeatherProvider = it },
onApiKeyChanged = { openWeatherMapApiKey = it }
)
Spacer(modifier = Modifier.padding(30.dp))
}
}
//added
@Composable
fun WeatherProviderSection(
selectedProvider: WeatherDataProvider,
apiKey: String,
onProviderChanged: (WeatherDataProvider) -> Unit,
onApiKeyChanged: (String) -> Unit
) {
val weatherProviderOptions = WeatherDataProvider.entries.toList()
.map { provider -> DropdownOption(provider.id, provider.label) }
val weatherProviderSelection by remember(selectedProvider) {
mutableStateOf(weatherProviderOptions.find { option -> option.id == selectedProvider.id }!!)
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Dropdown(
label = "Weather Provider",
options = weatherProviderOptions,
selected = weatherProviderSelection
) { selectedOption ->
onProviderChanged(WeatherDataProvider.entries.find { provider -> provider.id == selectedOption.id }!!)
}
if (selectedProvider == WeatherDataProvider.OPEN_WEATHER_MAP) {
OutlinedTextField(
value = apiKey,
onValueChange = { onApiKeyChanged(it) },
label = { Text("OpenWeatherMap API Key") },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
Text(
text = "If you want to use OpenWeatherMap, you need to provide an API key. If you don't provide any correct, you'll get OpenMeteo weather data.",
style = MaterialTheme.typography.bodySmall
)
}
}
}