Different unit support, fixes #2

This commit is contained in:
Henry Hiles 2023-12-29 10:08:48 -05:00
parent 985382fa1b
commit ae71058669
23 changed files with 304 additions and 67 deletions

View file

@ -1,6 +1,7 @@
package com.henryhiles.qweather.di package com.henryhiles.qweather.di
import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferenceManager
import com.henryhiles.qweather.presentation.screenmodel.UnitPreferenceManager
import com.henryhiles.qweather.presentation.screenmodel.LocationPreferenceManager import com.henryhiles.qweather.presentation.screenmodel.LocationPreferenceManager
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
@ -8,5 +9,6 @@ import org.koin.dsl.module
val managerModule = module { val managerModule = module {
singleOf(::AppearancePreferenceManager) singleOf(::AppearancePreferenceManager)
singleOf(::UnitPreferenceManager)
singleOf(::LocationPreferenceManager) singleOf(::LocationPreferenceManager)
} }

View file

@ -1,6 +1,7 @@
package com.henryhiles.qweather.di package com.henryhiles.qweather.di
import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel import com.henryhiles.qweather.presentation.screenmodel.AppearancePreferencesScreenModel
import com.henryhiles.qweather.presentation.screenmodel.UnitPreferencesScreenModel
import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
import com.henryhiles.qweather.presentation.screenmodel.LocationPickerScreenModel import com.henryhiles.qweather.presentation.screenmodel.LocationPickerScreenModel
@ -9,6 +10,7 @@ import org.koin.dsl.module
val screenModelModule = module { val screenModelModule = module {
factoryOf(::AppearancePreferencesScreenModel) factoryOf(::AppearancePreferencesScreenModel)
factoryOf(::UnitPreferencesScreenModel)
factoryOf(::LocationPickerScreenModel) factoryOf(::LocationPickerScreenModel)
factoryOf(::HourlyWeatherScreenModel) factoryOf(::HourlyWeatherScreenModel)
factoryOf(::DailyWeatherScreenModel) factoryOf(::DailyWeatherScreenModel)

View file

@ -1,7 +1,9 @@
package com.henryhiles.qweather.domain.mappers package com.henryhiles.qweather.domain.mappers
import com.henryhiles.qweather.domain.remote.DailyWeatherDataDto import com.henryhiles.qweather.domain.remote.DailyWeatherDataDto
import com.henryhiles.qweather.domain.remote.DailyWeatherUnitsDto
import com.henryhiles.qweather.domain.remote.HourlyWeatherDataDto import com.henryhiles.qweather.domain.remote.HourlyWeatherDataDto
import com.henryhiles.qweather.domain.remote.HourlyWeatherUnitsDto
import com.henryhiles.qweather.domain.remote.WeatherDto import com.henryhiles.qweather.domain.remote.WeatherDto
import com.henryhiles.qweather.domain.weather.DailyWeatherData import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherData import com.henryhiles.qweather.domain.weather.HourlyWeatherData
@ -12,7 +14,7 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt import kotlin.math.roundToInt
fun HourlyWeatherDataDto.toHourlyWeatherData(): List<HourlyWeatherData> { fun HourlyWeatherDataDto.toHourlyWeatherData(units: HourlyWeatherUnitsDto): List<HourlyWeatherData> {
return time.subList(0, 24).mapIndexed { index, time -> return time.subList(0, 24).mapIndexed { index, time ->
HourlyWeatherData( HourlyWeatherData(
time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME), time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME),
@ -21,11 +23,12 @@ fun HourlyWeatherDataDto.toHourlyWeatherData(): List<HourlyWeatherData> {
windSpeed = windSpeed[index].roundToInt(), windSpeed = windSpeed[index].roundToInt(),
precipitationProbability = precipitationProbability.getOrNull(index), precipitationProbability = precipitationProbability.getOrNull(index),
weatherType = WeatherType.fromWMO(weatherCode[index]), weatherType = WeatherType.fromWMO(weatherCode[index]),
units = units,
) )
} }
} }
fun DailyWeatherDataDto.toDailyWeatherData(): List<DailyWeatherData> { fun DailyWeatherDataDto.toDailyWeatherData(units: DailyWeatherUnitsDto): List<DailyWeatherData> {
return date.mapIndexed { index, date -> return date.mapIndexed { index, date ->
DailyWeatherData( DailyWeatherData(
date = LocalDate.parse(date, DateTimeFormatter.ISO_DATE), date = LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
@ -38,12 +41,17 @@ fun DailyWeatherDataDto.toDailyWeatherData(): List<DailyWeatherData> {
windSpeedMax = windSpeedMax[index].roundToInt(), windSpeedMax = windSpeedMax[index].roundToInt(),
sunrise = LocalDateTime.parse(sunrise[index]), sunrise = LocalDateTime.parse(sunrise[index]),
sunset = LocalDateTime.parse(sunset[index]), sunset = LocalDateTime.parse(sunset[index]),
precipitationSum = precipitationSum[index],
units = units.copy(
precipitationSum = units.precipitationSum.replace("inch", "\""),
windSpeedMax = units.windSpeedMax.replace("mp/h", "mph"),
)
) )
} }
} }
fun WeatherDto.toHourlyWeatherInfo(): HourlyWeatherInfo { fun WeatherDto.toHourlyWeatherInfo(): HourlyWeatherInfo {
val weatherDataMap = hourlyWeatherData.toHourlyWeatherData() val weatherDataMap = hourlyWeatherData.toHourlyWeatherData(units = hourlyUnits)
val now = LocalDateTime.now() val now = LocalDateTime.now()
val currentWeatherData = weatherDataMap.find { val currentWeatherData = weatherDataMap.find {
it.time.hour == now.hour it.time.hour == now.hour

View file

@ -26,3 +26,20 @@ data class DailyWeatherDataDto(
@SerialName("apparent_temperature_min") @SerialName("apparent_temperature_min")
val apparentTemperatureMin: List<Float> val apparentTemperatureMin: List<Float>
) )
@Serializable
data class DailyWeatherUnitsDto(
@SerialName("precipitation_probability_max")
val precipitationProbabilityMax: String,
@SerialName("precipitation_sum")
val precipitationSum: String,
@SerialName("windspeed_10m_max")
val windSpeedMax: String,
@SerialName("temperature_2m_max")
val temperatureMax: String,
@SerialName("temperature_2m_min")
val temperatureMin: String,
@SerialName("apparent_temperature_max")
val apparentTemperatureMax: String,
@SerialName("apparent_temperature_min")
val apparentTemperatureMin: String
)

View file

@ -17,3 +17,15 @@ data class HourlyWeatherDataDto(
@SerialName("windspeed_10m") @SerialName("windspeed_10m")
val windSpeed: List<Float>, val windSpeed: List<Float>,
) )
@Serializable
data class HourlyWeatherUnitsDto(
@SerialName("temperature_2m")
val temperature: String,
@SerialName("apparent_temperature")
val apparentTemperature: String,
@SerialName("precipitation_probability")
val precipitationProbability: String,
@SerialName("windspeed_10m")
val windSpeed: String,
)

View file

@ -1,5 +1,7 @@
package com.henryhiles.qweather.domain.remote package com.henryhiles.qweather.domain.remote
import com.henryhiles.qweather.presentation.screenmodel.PrecipitationUnit
import com.henryhiles.qweather.presentation.screenmodel.TempUnit
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Headers import retrofit2.http.Headers
import retrofit2.http.Query import retrofit2.http.Query
@ -17,6 +19,9 @@ interface WeatherApi {
suspend fun getWeatherData( suspend fun getWeatherData(
@Query("latitude") lat: Float, @Query("latitude") lat: Float,
@Query("longitude") long: Float, @Query("longitude") long: Float,
@Query("temperature_unit") tempUnit: String,
@Query("wind_speed_unit") windUnit: String,
@Query("precipitation_unit") precipitationUnit: String,
): WeatherDto ): WeatherDto
@Headers("Cache-Control: no-cache") @Headers("Cache-Control: no-cache")
@ -24,5 +29,8 @@ interface WeatherApi {
suspend fun getWeatherDataWithoutCache( suspend fun getWeatherDataWithoutCache(
@Query("latitude") lat: Float, @Query("latitude") lat: Float,
@Query("longitude") long: Float, @Query("longitude") long: Float,
@Query("temperature_unit") tempUnit: String,
@Query("wind_speed_unit") windUnit: String,
@Query("precipitation_unit") precipitationUnit: String,
): WeatherDto ): WeatherDto
} }

View file

@ -8,6 +8,12 @@ data class WeatherDto(
@SerialName("hourly") @SerialName("hourly")
val hourlyWeatherData: HourlyWeatherDataDto, val hourlyWeatherData: HourlyWeatherDataDto,
@SerialName("hourly_units")
val hourlyUnits: HourlyWeatherUnitsDto,
@SerialName("daily") @SerialName("daily")
val dailyWeatherData: DailyWeatherDataDto val dailyWeatherData: DailyWeatherDataDto,
@SerialName("daily_units")
val dailyUnits: DailyWeatherUnitsDto,
) )

View file

@ -1,28 +1,48 @@
package com.henryhiles.qweather.domain.repository package com.henryhiles.qweather.domain.repository
import androidx.compose.ui.text.toLowerCase
import com.henryhiles.qweather.domain.mappers.toDailyWeatherData import com.henryhiles.qweather.domain.mappers.toDailyWeatherData
import com.henryhiles.qweather.domain.mappers.toHourlyWeatherInfo import com.henryhiles.qweather.domain.mappers.toHourlyWeatherInfo
import com.henryhiles.qweather.domain.remote.WeatherApi import com.henryhiles.qweather.domain.remote.WeatherApi
import com.henryhiles.qweather.domain.remote.WeatherDto
import com.henryhiles.qweather.domain.util.Resource import com.henryhiles.qweather.domain.util.Resource
import com.henryhiles.qweather.domain.weather.DailyWeatherData import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherInfo import com.henryhiles.qweather.domain.weather.HourlyWeatherInfo
import com.henryhiles.qweather.presentation.screenmodel.UnitPreferenceManager
class WeatherRepository(private val api: WeatherApi) { class WeatherRepository(private val api: WeatherApi) {
suspend fun getHourlyWeatherData( private suspend fun getWeatherData(
lat: Float, lat: Float,
long: Float, long: Float,
units: UnitPreferenceManager,
cache: Boolean = true,
): WeatherDto {
return if (cache) api.getWeatherData(
lat,
long,
units.tempUnit.name.lowercase(),
units.windUnit.name.lowercase(),
units.precipitationUnit.name.lowercase(),
) else api.getWeatherDataWithoutCache(
lat,
long,
units.tempUnit.name,
units.windUnit.name,
units.precipitationUnit.name,
)
}
suspend fun getDailyWeatherData(
lat: Float,
long: Float,
units: UnitPreferenceManager,
cache: Boolean = true cache: Boolean = true
): Resource<HourlyWeatherInfo> { ): Resource<List<DailyWeatherData>> {
return try { return try {
Resource.Success( Resource.Success(
data = ( with(getWeatherData(lat, long, units, cache)) {
if (cache) api.getWeatherData( dailyWeatherData.toDailyWeatherData(dailyUnits)
lat = lat, }
long = long
) else api.getWeatherDataWithoutCache(
lat = lat,
long = long
)).toHourlyWeatherInfo()
) )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@ -30,20 +50,15 @@ class WeatherRepository(private val api: WeatherApi) {
} }
} }
suspend fun getDailyWeatherData( suspend fun getHourlyWeatherData(
lat: Float, lat: Float,
long: Float, long: Float,
units: UnitPreferenceManager,
cache: Boolean = true cache: Boolean = true
): Resource<List<DailyWeatherData>> { ): Resource<HourlyWeatherInfo> {
return try { return try {
Resource.Success( Resource.Success(
(if (cache) api.getWeatherData( getWeatherData(lat, long, units, cache).toHourlyWeatherInfo()
lat = lat,
long = long
) else api.getWeatherDataWithoutCache(
lat = lat,
long = long
)).dailyWeatherData.toDailyWeatherData()
) )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()

View file

@ -1,10 +1,12 @@
package com.henryhiles.qweather.domain.weather package com.henryhiles.qweather.domain.weather
import com.henryhiles.qweather.domain.remote.DailyWeatherUnitsDto
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
data class DailyWeatherData( data class DailyWeatherData(
val date: LocalDate, val date: LocalDate,
val precipitationSum: Float,
val weatherType: WeatherType, val weatherType: WeatherType,
val sunrise: LocalDateTime, val sunrise: LocalDateTime,
val sunset: LocalDateTime, val sunset: LocalDateTime,
@ -13,5 +15,6 @@ data class DailyWeatherData(
val apparentTemperatureMax: Int, val apparentTemperatureMax: Int,
val apparentTemperatureMin: Int, val apparentTemperatureMin: Int,
val precipitationProbabilityMax: Int?, val precipitationProbabilityMax: Int?,
val windSpeedMax: Int val windSpeedMax: Int,
val units: DailyWeatherUnitsDto,
) )

View file

@ -1,5 +1,6 @@
package com.henryhiles.qweather.domain.weather package com.henryhiles.qweather.domain.weather
import com.henryhiles.qweather.domain.remote.HourlyWeatherUnitsDto
import java.time.LocalDateTime import java.time.LocalDateTime
data class HourlyWeatherData( data class HourlyWeatherData(
@ -9,4 +10,5 @@ data class HourlyWeatherData(
val weatherType: WeatherType, val weatherType: WeatherType,
val precipitationProbability: Int?, val precipitationProbability: Int?,
val windSpeed: Int, val windSpeed: Int,
val units: HourlyWeatherUnitsDto,
) )

View file

@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
@Composable @Composable
inline fun <reified E : Enum<E>> SettingsItemChoice( inline fun <reified E : Enum<E>> SettingsItemChoice(
label: String, label: String,
secondaryLabel: String? = null,
title: String = label, title: String = label,
disabled: Boolean = false, disabled: Boolean = false,
pref: E, pref: E,
@ -23,6 +24,7 @@ inline fun <reified E : Enum<E>> SettingsItemChoice(
SettingItem( SettingItem(
modifier = Modifier.clickable { opened = true }, modifier = Modifier.clickable { opened = true },
text = { Text(text = label) }, text = { Text(text = label) },
secondaryText = { if (secondaryLabel != null) Text(text = secondaryLabel) }
) { ) {
SettingsChoiceDialog( SettingsChoiceDialog(
visible = opened, visible = opened,

View file

@ -15,15 +15,22 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.henryhiles.qweather.R
import com.henryhiles.qweather.domain.util.getIcon import com.henryhiles.qweather.domain.util.getIcon
import com.henryhiles.qweather.domain.weather.DailyWeatherData import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherData import com.henryhiles.qweather.domain.weather.HourlyWeatherData
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@Composable @Composable
fun WeatherCard(hour: HourlyWeatherData, dailyData: DailyWeatherData, modifier: Modifier = Modifier) { fun WeatherCard(
hour: HourlyWeatherData,
dailyData: DailyWeatherData,
modifier: Modifier = Modifier
) {
val formattedTime = remember(hour) { val formattedTime = remember(hour) {
hour.time.format(DateTimeFormatter.ofPattern("HH:mm")) hour.time.format(DateTimeFormatter.ofPattern("HH:mm"))
} }
@ -48,14 +55,23 @@ fun WeatherCard(hour: HourlyWeatherData, dailyData: DailyWeatherData, modifier:
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Image( Image(
painter = painterResource(id = getIcon(hour, dailyData)), painter = painterResource(id = getIcon(hour, dailyData)),
contentDescription = "Image of ${hour.weatherType.weatherDesc}", contentDescription = hour.weatherType.weatherDesc,
modifier = Modifier.height(140.dp), modifier = Modifier.height(140.dp),
contentScale = ContentScale.FillHeight contentScale = ContentScale.FillHeight
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text(text = "${hour.temperature}°C", fontSize = 50.sp) Text(text = "${hour.temperature}${hour.units.temperature}", fontSize = 50.sp)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text(text = "${hour.weatherType.weatherDesc} - Feels like ${hour.apparentTemperature}°C", fontSize = 20.sp) Text(
text = stringResource(
id = R.string.weather_description,
hour.weatherType.weatherDesc,
hour.apparentTemperature,
hour.units.apparentTemperature
),
fontSize = 20.sp,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -63,15 +79,15 @@ fun WeatherCard(hour: HourlyWeatherData, dailyData: DailyWeatherData, modifier:
) { ) {
WeatherDataDisplay( WeatherDataDisplay(
value = hour.precipitationProbability, value = hour.precipitationProbability,
unit = "%", unit = hour.units.precipitationProbability,
icon = Icons.Outlined.WaterDrop, icon = Icons.Outlined.WaterDrop,
description = "Chance of precipitation" description = stringResource(id = R.string.precipitation_probability)
) )
WeatherDataDisplay( WeatherDataDisplay(
value = hour.windSpeed, value = hour.windSpeed,
unit = "km/h", unit = hour.units.windSpeed,
icon = Icons.Outlined.WindPower, icon = Icons.Outlined.WindPower,
description = "Wind Speed", description = stringResource(id = R.string.wind_speed),
) )
} }
} }

View file

@ -16,7 +16,7 @@ import com.henryhiles.qweather.R
@Composable @Composable
fun WeatherDataDisplay( fun WeatherDataDisplay(
value: Int?, value: Any?,
unit: String, unit: String,
icon: ImageVector, icon: ImageVector,
description: String, description: String,

View file

@ -16,8 +16,10 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.henryhiles.qweather.R
import com.henryhiles.qweather.domain.weather.DailyWeatherData import com.henryhiles.qweather.domain.weather.DailyWeatherData
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -40,7 +42,7 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) {
) { ) {
Image( Image(
painter = painterResource(id = dailyWeatherData.weatherType.iconRes), painter = painterResource(id = dailyWeatherData.weatherType.iconRes),
contentDescription = "Image of ${dailyWeatherData.weatherType.weatherDesc}", contentDescription = dailyWeatherData.weatherType.weatherDesc,
modifier = Modifier.width(48.dp) modifier = Modifier.width(48.dp)
) )
@ -50,12 +52,18 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) {
text = formattedDate, text = formattedDate,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
Text(text = "Feels like ${dailyWeatherData.apparentTemperatureMax}°C") Text(
text = stringResource(
id = R.string.feels_like,
dailyWeatherData.apparentTemperatureMax,
dailyWeatherData.units.apparentTemperatureMax
)
)
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
text = "${dailyWeatherData.temperatureMax}°C", text = "${dailyWeatherData.temperatureMax}${dailyWeatherData.units.temperatureMax}",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
) )
} }
@ -67,23 +75,21 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData) {
) { ) {
WeatherDataDisplay( WeatherDataDisplay(
value = dailyWeatherData.precipitationProbabilityMax, value = dailyWeatherData.precipitationProbabilityMax,
unit = "%", unit = dailyWeatherData.units.precipitationProbabilityMax,
icon = Icons.Outlined.WaterDrop, icon = Icons.Outlined.WaterDrop,
description = "Chance of rain" description = stringResource(id = R.string.precipitation_probability)
) )
Spacer(modifier = Modifier.width(16.dp))
WeatherDataDisplay( WeatherDataDisplay(
value = dailyWeatherData.windSpeedMax, value = dailyWeatherData.precipitationSum,
unit = "mm", unit = dailyWeatherData.units.precipitationSum,
icon = Icons.Outlined.Water, icon = Icons.Outlined.Water,
description = "Precipitation Amount" description = stringResource(id = R.string.precipitation_amount)
) )
Spacer(modifier = Modifier.width(16.dp))
WeatherDataDisplay( WeatherDataDisplay(
value = dailyWeatherData.windSpeedMax, value = dailyWeatherData.windSpeedMax,
unit = "km/h", unit = dailyWeatherData.units.windSpeedMax,
icon = Icons.Outlined.WindPower, icon = Icons.Outlined.WindPower,
description = "Wind Speed" description = stringResource(id = R.string.wind_speed)
) )
} }
} }

View file

@ -44,10 +44,10 @@ fun WeatherHour(
painter = painterResource( painter = painterResource(
id = getIcon(it, dailyData) id = getIcon(it, dailyData)
), ),
contentDescription = "Image of ${it.weatherType.weatherDesc}", contentDescription = it.weatherType.weatherDesc,
modifier = Modifier.width(40.dp) modifier = Modifier.width(40.dp)
) )
Text(text = "${it.temperature}°C") Text(text = "${it.temperature}${it.units.temperature}")
} }
} }
} }

View file

@ -46,19 +46,19 @@ fun WeatherToday(data: DailyWeatherData) {
) { ) {
WeatherDataDisplay( WeatherDataDisplay(
value = data.temperatureMax, value = data.temperatureMax,
unit = "°C", unit = data.units.temperatureMax,
icon = Icons.Default.ArrowUpward, icon = Icons.Default.ArrowUpward,
description = stringResource(R.string.weather_high, data.temperatureMax) description = stringResource(R.string.weather_high, data.temperatureMax, data.units.temperatureMax)
) )
WeatherDataDisplay( WeatherDataDisplay(
value = data.temperatureMin, value = data.temperatureMin,
unit = "°C", unit = data.units.temperatureMin,
icon = Icons.Default.ArrowDownward, icon = Icons.Default.ArrowDownward,
description = stringResource(id = R.string.weather_low, data.temperatureMin) description = stringResource(id = R.string.weather_low, data.temperatureMin, data.units.temperatureMin)
) )
WeatherDataDisplay( WeatherDataDisplay(
value = data.precipitationProbabilityMax, value = data.precipitationProbabilityMax,
unit = "%", unit = data.units.precipitationProbabilityMax,
icon = Icons.Outlined.WaterDrop, icon = Icons.Outlined.WaterDrop,
description = data.precipitationProbabilityMax?.let { description = data.precipitationProbabilityMax?.let {
stringResource( stringResource(

View file

@ -0,0 +1,60 @@
package com.henryhiles.qweather.presentation.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.components.navigation.SmallToolbar
import com.henryhiles.qweather.presentation.components.settings.SettingsItemChoice
import com.henryhiles.qweather.presentation.screenmodel.UnitPreferencesScreenModel
class UnitsScreen : Screen {
@Composable
override fun Content(){
val screenModel: UnitPreferencesScreenModel = getScreenModel()
val context = LocalContext.current
Scaffold(topBar = {
SmallToolbar(
title = { Text(text = stringResource(R.string.settings_units)) },
backButton = true
)
}) { padding ->
Column(
modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
) {
SettingsItemChoice(
label = stringResource(R.string.unit_temp),
secondaryLabel = stringResource(R.string.unit_temp_desc),
pref = screenModel.prefs.tempUnit,
labelFactory = { context.getString(it.label) }
) { screenModel.prefs.tempUnit = it }
SettingsItemChoice(
label = stringResource(R.string.unit_wind),
secondaryLabel = stringResource(R.string.unit_wind_desc),
pref = screenModel.prefs.windUnit,
labelFactory = { context.getString(it.label) }
) { screenModel.prefs.windUnit = it }
SettingsItemChoice(
label = stringResource(R.string.unit_precipitation),
secondaryLabel = stringResource(R.string.unit_precipitation_desc),
pref = screenModel.prefs.precipitationUnit,
labelFactory = { context.getString(it.label) }
) { screenModel.prefs.precipitationUnit = it }
}
}
}
}

View file

@ -19,7 +19,8 @@ data class DailyWeatherState(
class DailyWeatherScreenModel( class DailyWeatherScreenModel(
private val repository: WeatherRepository, private val repository: WeatherRepository,
val locationPreferenceManager: LocationPreferenceManager private val unitsPreferenceManager: UnitPreferenceManager,
val locationPreferenceManager: LocationPreferenceManager,
) : ScreenModel { ) : ScreenModel {
var state by mutableStateOf(DailyWeatherState()) var state by mutableStateOf(DailyWeatherState())
private set private set
@ -31,7 +32,8 @@ class DailyWeatherScreenModel(
state = when (val result = repository.getDailyWeatherData( state = when (val result = repository.getDailyWeatherData(
lat = location.latitude, lat = location.latitude,
long = location.longitude, long = location.longitude,
cache = cache cache = cache,
units = unitsPreferenceManager,
)) { )) {
is Resource.Success -> { is Resource.Success -> {
state.copy( state.copy(

View file

@ -19,6 +19,7 @@ data class HourlyWeatherState(
class HourlyWeatherScreenModel( class HourlyWeatherScreenModel(
private val repository: WeatherRepository, private val repository: WeatherRepository,
private val unitsPreferenceManager: UnitPreferenceManager,
val locationPreferenceManager: LocationPreferenceManager, val locationPreferenceManager: LocationPreferenceManager,
) : ScreenModel { ) : ScreenModel {
var state by mutableStateOf(HourlyWeatherState()) var state by mutableStateOf(HourlyWeatherState())
@ -32,6 +33,7 @@ class HourlyWeatherScreenModel(
repository.getHourlyWeatherData( repository.getHourlyWeatherData(
lat = location.latitude, lat = location.latitude,
long = location.longitude, long = location.longitude,
units = unitsPreferenceManager,
cache = cache cache = cache
)) { )) {
is Resource.Success -> { is Resource.Success -> {

View file

@ -0,0 +1,36 @@
package com.henryhiles.qweather.presentation.screenmodel
import android.content.Context
import androidx.annotation.StringRes
import cafe.adriel.voyager.core.model.ScreenModel
import com.henryhiles.qweather.R
import com.henryhiles.qweather.domain.manager.BasePreferenceManager
class UnitPreferenceManager(context: Context) :
BasePreferenceManager(context.getSharedPreferences("prefs", Context.MODE_PRIVATE)) {
var tempUnit by enumPreference("temp_unit", TempUnit.CELSIUS)
var windUnit by enumPreference("wind_unit", WindUnit.KMH)
var precipitationUnit by enumPreference("precipitation_unit", PrecipitationUnit.MM)
}
enum class TempUnit(@StringRes val label: Int) {
CELSIUS(R.string.celsius),
FAHRENHEIT(R.string.fahrenheit),
}
enum class WindUnit(@StringRes val label: Int) {
KMH(R.string.kmh),
MS(R.string.ms),
MPH(R.string.mph),
KN(R.string.kn),
}
enum class PrecipitationUnit(@StringRes val label: Int) {
MM(R.string.mm),
INCH(R.string.inch)
}
class UnitPreferencesScreenModel(
val prefs: UnitPreferenceManager
) : ScreenModel

View file

@ -3,6 +3,7 @@ package com.henryhiles.qweather.presentation.tabs
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.AcUnit
import androidx.compose.material.icons.outlined.GpsFixed import androidx.compose.material.icons.outlined.GpsFixed
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Palette
@ -20,6 +21,7 @@ import com.henryhiles.qweather.presentation.components.settings.SettingsCategory
import com.henryhiles.qweather.presentation.screen.AboutScreen import com.henryhiles.qweather.presentation.screen.AboutScreen
import com.henryhiles.qweather.presentation.screen.AppearanceSettingsScreen import com.henryhiles.qweather.presentation.screen.AppearanceSettingsScreen
import com.henryhiles.qweather.presentation.screen.LocationPickerScreen import com.henryhiles.qweather.presentation.screen.LocationPickerScreen
import com.henryhiles.qweather.presentation.screen.UnitsScreen
object SettingsTab : NavigationTab { object SettingsTab : NavigationTab {
override val options: TabOptions override val options: TabOptions
@ -52,6 +54,12 @@ object SettingsTab : NavigationTab {
subtext = stringResource(R.string.settings_location_description), subtext = stringResource(R.string.settings_location_description),
destination = ::LocationPickerScreen destination = ::LocationPickerScreen
) )
SettingsCategory(
icon = Icons.Outlined.AcUnit,
text = stringResource(R.string.settings_units),
subtext = stringResource(R.string.settings_units_description),
destination = ::UnitsScreen
)
} }
} }

View file

@ -46,15 +46,15 @@ object WeekTab : NavigationTab {
@Composable @Composable
override fun Content() { override fun Content() {
val dailyWeatherViewModel = getScreenModel<DailyWeatherScreenModel>() val weatherViewModel = getScreenModel<DailyWeatherScreenModel>()
LaunchedEffect(key1 = dailyWeatherViewModel.locationPreferenceManager.selectedIndex) { LaunchedEffect(key1 = weatherViewModel.locationPreferenceManager.selectedIndex) {
dailyWeatherViewModel.loadWeatherInfo() weatherViewModel.loadWeatherInfo()
} }
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
when { when {
dailyWeatherViewModel.state.isLoading -> { weatherViewModel.state.isLoading -> {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.align( modifier = Modifier.align(
Alignment.Center Alignment.Center
@ -62,15 +62,19 @@ object WeekTab : NavigationTab {
) )
} }
dailyWeatherViewModel.state.error != null -> { weatherViewModel.state.error != null -> {
AlertDialog( AlertDialog(
onDismissRequest = {}, onDismissRequest = {},
confirmButton = {}, confirmButton = {
title = { Text(text = stringResource(R.string.error)) }, TextButton(onClick = { weatherViewModel.loadWeatherInfo() }) {
Text(text = stringResource(id = R.string.action_try_again))
}
},
title = { Text(text = stringResource(id = R.string.error)) },
text = { text = {
SelectionContainer { SelectionContainer {
Text( Text(
text = dailyWeatherViewModel.state.error!!, text = weatherViewModel.state.error!!,
) )
} }
}, },
@ -79,7 +83,7 @@ object WeekTab : NavigationTab {
else -> { else -> {
LazyColumn(contentPadding = PaddingValues(16.dp)) { LazyColumn(contentPadding = PaddingValues(16.dp)) {
dailyWeatherViewModel.state.dailyWeatherData?.let { data -> weatherViewModel.state.dailyWeatherData?.let { data ->
item { WeatherToday(data = data[0]) } item { WeatherToday(data = data[0]) }
items(data) { items(data) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -94,10 +98,10 @@ object WeekTab : NavigationTab {
@Composable @Composable
override fun Actions() { override fun Actions() {
val dailyWeatherViewModel = getScreenModel<DailyWeatherScreenModel>() val weatherViewModel = getScreenModel<DailyWeatherScreenModel>()
IconButton(onClick = { IconButton(onClick = {
dailyWeatherViewModel.loadWeatherInfo(cache = false) weatherViewModel.loadWeatherInfo(cache = false)
}) { }) {
Icon( Icon(
imageVector = Icons.Filled.Refresh, imageVector = Icons.Filled.Refresh,

View file

@ -23,6 +23,8 @@
<string name="settings_appearance_description">Theme, dynamic colors</string> <string name="settings_appearance_description">Theme, dynamic colors</string>
<string name="settings_location">Location</string> <string name="settings_location">Location</string>
<string name="settings_location_description">Location to fetch data from</string> <string name="settings_location_description">Location to fetch data from</string>
<string name="settings_units">Units</string>
<string name="settings_units_description">Units to fetch data in, e.g. imperial/metric</string>
<string name="location">Location</string> <string name="location">Location</string>
<string name="locations">Locations</string> <string name="locations">Locations</string>
@ -34,9 +36,33 @@
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
<string name="weather_high">High: %1$d°C</string>
<string name="weather_low">Low: %1$d°C</string> <string name="unit_temp">Temperature Unit</string>
<string name="celsius">Celsius (C°)</string>
<string name="fahrenheit">Fahrenheit (F°)</string>
<string name="unit_temp_desc">Celsius, Fahrenheit</string>
<string name="unit_wind">Wind Speed Unit</string>
<string name="kmh">Kilometers per Hour (km/h)</string>
<string name="ms">Meters per second (m/s)</string>
<string name="mph">Miles per hour (mph)</string>
<string name="kn">Knots (kn)</string>
<string name="unit_wind_desc">Km/h, m/s, Mph, Knots</string>
<string name="unit_precipitation">Precipitation Unit</string>
<string name="mm">Millimeters (mm)</string>
<string name="inch">Inches (in)</string>
<string name="unit_precipitation_desc">Millimeters, Inches</string>
<string name="weather_high">High: %1$d%2$s</string>
<string name="weather_low">Low: %1$d%2$s</string>
<string name="feels_like">Feels like %1$d%2$s</string>
<string name="weather_precipitation">Precipitation: %1$d&#65130;</string> <string name="weather_precipitation">Precipitation: %1$d&#65130;</string>
<string name="weather_description">%1$s - Feels like %2$d%3$s</string>
<string name="precipitation_probability">Precipitation Probability</string>
<string name="precipitation_amount">Precipitation Amount</string>
<string name="wind_speed">Wind Speed</string>
<string name="today_in">Today in %1$s</string> <string name="today_in">Today in %1$s</string>