Many bug fixes, nearing release

This commit is contained in:
Henry Hiles 2023-12-27 08:31:49 -05:00
parent 92bad19a90
commit 985382fa1b
18 changed files with 164 additions and 129 deletions

View file

@ -1,5 +1,5 @@
plugins {
kotlin("plugin.serialization") version "1.9.21"
kotlin("plugin.serialization")
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")

View file

@ -6,7 +6,7 @@ import com.henryhiles.qweather.domain.geocoding.GeocodingData
fun GeocodingDto.toGeocodingData(): List<GeocodingData> {
return results.map {
GeocodingData(
location = "${it.city}, ${it.admin}, ${it.country}",
location = "${if(it.name == it.country) "" else "${it.name}, "}${if(it.admin == null) "" else "${it.admin}, "}${it.country}",
longitude = it.longitude,
latitude = it.latitude,
)

View file

@ -35,7 +35,9 @@ fun DailyWeatherDataDto.toDailyWeatherData(): List<DailyWeatherData> {
temperatureMax = temperatureMax[index].roundToInt(),
temperatureMin = temperatureMin[index].roundToInt(),
precipitationProbabilityMax = precipitationProbabilityMax.getOrNull(index),
windSpeedMax = windSpeedMax[index].roundToInt()
windSpeedMax = windSpeedMax[index].roundToInt(),
sunrise = LocalDateTime.parse(sunrise[index]),
sunset = LocalDateTime.parse(sunset[index]),
)
}
}
@ -49,8 +51,5 @@ fun WeatherDto.toHourlyWeatherInfo(): HourlyWeatherInfo {
return HourlyWeatherInfo(
weatherData = weatherDataMap,
currentWeatherData = currentWeatherData,
highTemperature = weatherDataMap.maxBy { it.temperature }.temperature,
lowTemperature = weatherDataMap.minBy { it.temperature }.temperature,
precipitationProbability = weatherDataMap.maxBy { it.precipitationProbability ?: 0}.precipitationProbability
)
}

View file

@ -9,6 +9,8 @@ data class DailyWeatherDataDto(
val date: List<String>,
@SerialName("weathercode")
val weatherCode: List<Int>,
val sunrise: List<String>,
val sunset: List<String>,
@SerialName("precipitation_probability_max")
val precipitationProbabilityMax: List<Int?>,
@SerialName("precipitation_sum")

View file

@ -3,6 +3,7 @@ package com.henryhiles.qweather.domain.remote
import kotlinx.serialization.Serializable
@Serializable
data class GeocodingDto(
data class
GeocodingDto(
val results: List<GeocodingLocationDto> = listOf()
)

View file

@ -5,11 +5,10 @@ import kotlinx.serialization.Serializable
@Serializable
data class GeocodingLocationDto(
@SerialName("name")
val city: String,
val name: String,
val country: String,
@SerialName("admin1")
val admin: String,
val admin: String? = null,
val latitude: Float,
val longitude: Float
)
)

View file

@ -5,7 +5,7 @@ import retrofit2.http.Headers
import retrofit2.http.Query
const val DAILY =
"daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_sum,precipitation_probability_max,windspeed_10m_max"
"daily=weathercode,sunrise,sunset,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_sum,precipitation_probability_max,windspeed_10m_max"
const val HOURLY =
"hourly=temperature_2m,apparent_temperature,precipitation_probability,weathercode,windspeed_10m"
const val TIMEZONE = "timezone=auto"

View file

@ -0,0 +1,14 @@
package com.henryhiles.qweather.domain.util
import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherData
fun getIcon(
data: HourlyWeatherData,
dailyData: DailyWeatherData,
): Int {
return if (data.time.isAfter(dailyData.sunrise) && data.time.isBefore(
dailyData.sunset
)
) data.weatherType.iconRes else data.weatherType.nightIconRes
}

View file

@ -1,10 +1,13 @@
package com.henryhiles.qweather.domain.weather
import java.time.LocalDate
import java.time.LocalDateTime
data class DailyWeatherData(
val date: LocalDate,
val weatherType: WeatherType,
val sunrise: LocalDateTime,
val sunset: LocalDateTime,
val temperatureMax: Int,
val temperatureMin: Int,
val apparentTemperatureMax: Int,

View file

@ -1,6 +1,5 @@
package com.henryhiles.qweather.domain.weather
import androidx.annotation.DrawableRes
import java.time.LocalDateTime
data class HourlyWeatherData(
@ -10,5 +9,4 @@ data class HourlyWeatherData(
val weatherType: WeatherType,
val precipitationProbability: Int?,
val windSpeed: Int,
@DrawableRes val icon: Int = if(time.hour < 8 || time.hour >= 19) weatherType.nightIconRes else weatherType.iconRes
)

View file

@ -3,7 +3,4 @@ package com.henryhiles.qweather.domain.weather
data class HourlyWeatherInfo(
val weatherData: List<HourlyWeatherData>,
val currentWeatherData: HourlyWeatherData?,
val highTemperature: Int,
val lowTemperature: Int,
val precipitationProbability: Int?
)

View file

@ -17,62 +17,62 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.henryhiles.qweather.domain.util.getIcon
import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherData
import java.time.format.DateTimeFormatter
@Composable
fun WeatherCard(hour: HourlyWeatherData?, modifier: Modifier = Modifier) {
hour?.let {
val formattedTime = remember(it) {
it.time.format(DateTimeFormatter.ofPattern("HH:mm"))
}
Card(
shape = RoundedCornerShape(8.dp),
modifier = modifier
fun WeatherCard(hour: HourlyWeatherData, dailyData: DailyWeatherData, modifier: Modifier = Modifier) {
val formattedTime = remember(hour) {
hour.time.format(DateTimeFormatter.ofPattern("HH:mm"))
}
Card(
shape = RoundedCornerShape(8.dp),
modifier = modifier
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
Row(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = formattedTime,
style = MaterialTheme.typography.headlineSmall,
)
}
Spacer(modifier = Modifier.height(16.dp))
Image(
painter = painterResource(id = it.icon),
contentDescription = "Image of ${it.weatherType.weatherDesc}",
modifier = Modifier.height(140.dp),
contentScale = ContentScale.FillHeight
Text(
text = formattedTime,
style = MaterialTheme.typography.headlineSmall,
)
}
Spacer(modifier = Modifier.height(16.dp))
Image(
painter = painterResource(id = getIcon(hour, dailyData)),
contentDescription = "Image of ${hour.weatherType.weatherDesc}",
modifier = Modifier.height(140.dp),
contentScale = ContentScale.FillHeight
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = "${hour.temperature}°C", fontSize = 50.sp)
Spacer(modifier = Modifier.height(16.dp))
Text(text = "${hour.weatherType.weatherDesc} - Feels like ${hour.apparentTemperature}°C", fontSize = 20.sp)
Spacer(modifier = Modifier.height(32.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
WeatherDataDisplay(
value = hour.precipitationProbability,
unit = "%",
icon = Icons.Outlined.WaterDrop,
description = "Chance of precipitation"
)
WeatherDataDisplay(
value = hour.windSpeed,
unit = "km/h",
icon = Icons.Outlined.WindPower,
description = "Wind Speed",
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = "${it.temperature}°C", fontSize = 50.sp)
Spacer(modifier = Modifier.height(16.dp))
Text(text = "${it.weatherType.weatherDesc} - Feels like ${it.apparentTemperature}°C", fontSize = 20.sp)
Spacer(modifier = Modifier.height(32.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
WeatherDataDisplay(
value = it.precipitationProbability,
unit = "%",
icon = Icons.Outlined.WaterDrop,
description = "Chance of precipitation"
)
WeatherDataDisplay(
value = it.windSpeed,
unit = "km/h",
icon = Icons.Outlined.WindPower,
description = "Wind Speed",
)
}
}
}
}

View file

@ -6,12 +6,14 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherState
import java.time.LocalDateTime
@Composable
fun WeatherForecast(
state: HourlyWeatherState,
dailyData: DailyWeatherData,
modifier: Modifier = Modifier,
onChangeSelected: (Int) -> Unit
) {
@ -20,10 +22,12 @@ fun WeatherForecast(
items(it.subList(LocalDateTime.now().hour, it.size)) {
WeatherHour(
data = it,
dailyData = dailyData,
modifier = Modifier
.padding(horizontal = 8.dp)
) { onChangeSelected(it.time.hour) }
}
}
}
}

View file

@ -13,12 +13,15 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.henryhiles.qweather.domain.util.getIcon
import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.domain.weather.HourlyWeatherData
import java.time.format.DateTimeFormatter
@Composable
fun WeatherHour(
data: HourlyWeatherData,
dailyData: DailyWeatherData,
modifier: Modifier = Modifier,
onChangeSelected: () -> Unit
) {
@ -38,7 +41,9 @@ fun WeatherHour(
) {
Text(text = formattedTime)
Image(
painter = painterResource(id = it.icon),
painter = painterResource(
id = getIcon(it, dailyData)
),
contentDescription = "Image of ${it.weatherType.weatherDesc}",
modifier = Modifier.width(40.dp)
)

View file

@ -15,60 +15,58 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.henryhiles.qweather.R
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherState
import com.henryhiles.qweather.domain.weather.DailyWeatherData
import com.henryhiles.qweather.presentation.screenmodel.LocationPreferenceManager
import org.koin.compose.koinInject
@Composable
fun WeatherToday(state: HourlyWeatherState) {
fun WeatherToday(data: DailyWeatherData) {
val locationPreferenceManager: LocationPreferenceManager = koinInject()
state.hourlyWeatherInfo?.let {
Card(
shape = RoundedCornerShape(8.dp),
Card(
shape = RoundedCornerShape(8.dp),
) {
Column(
modifier = Modifier.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
Text(
text = stringResource(id = R.string.today_in, with(locationPreferenceManager) {
locations.getOrNull(selectedIndex)?.location?.split(",")?.first()
?: stringResource(id = R.string.unknown)
}),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.height(24.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = stringResource(id = R.string.today_in, with(locationPreferenceManager) {
locations.getOrNull(selectedIndex)?.location?.split(",")?.first()
?: stringResource(id = R.string.unknown)
}),
style = MaterialTheme.typography.headlineSmall
WeatherDataDisplay(
value = data.temperatureMax,
unit = "°C",
icon = Icons.Default.ArrowUpward,
description = stringResource(R.string.weather_high, data.temperatureMax)
)
WeatherDataDisplay(
value = data.temperatureMin,
unit = "°C",
icon = Icons.Default.ArrowDownward,
description = stringResource(id = R.string.weather_low, data.temperatureMin)
)
WeatherDataDisplay(
value = data.precipitationProbabilityMax,
unit = "%",
icon = Icons.Outlined.WaterDrop,
description = data.precipitationProbabilityMax?.let {
stringResource(
id = R.string.weather_precipitation,
it
)
} ?: stringResource(id = R.string.unknown)
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.height(24.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
WeatherDataDisplay(
value = it.highTemperature,
unit = "°C",
icon = Icons.Default.ArrowUpward,
description = stringResource(R.string.weather_high, it.highTemperature)
)
WeatherDataDisplay(
value = it.lowTemperature,
unit = "°C",
icon = Icons.Default.ArrowDownward,
description = stringResource(id = R.string.weather_low, it.lowTemperature)
)
WeatherDataDisplay(
value = it.precipitationProbability,
unit = "%",
icon = Icons.Outlined.WaterDrop,
description = it.precipitationProbability?.let {
stringResource(
id = R.string.weather_precipitation,
it
)
} ?: stringResource(id = R.string.unknown)
)
}
}
}
}

View file

@ -21,6 +21,7 @@ import com.henryhiles.qweather.domain.util.NavigationTab
import com.henryhiles.qweather.presentation.components.weather.WeatherCard
import com.henryhiles.qweather.presentation.components.weather.WeatherForecast
import com.henryhiles.qweather.presentation.components.weather.WeatherToday
import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
object TodayTab : NavigationTab {
@ -42,9 +43,11 @@ object TodayTab : NavigationTab {
@Composable
override fun Content() {
val weatherViewModel = getScreenModel<HourlyWeatherScreenModel>()
val dailyWeatherViewModel = getScreenModel<DailyWeatherScreenModel>()
LaunchedEffect(key1 = weatherViewModel.locationPreferenceManager.selectedIndex) {
weatherViewModel.loadWeatherInfo()
dailyWeatherViewModel.loadWeatherInfo()
}
Box(modifier = Modifier.fillMaxSize()) {
@ -56,6 +59,7 @@ object TodayTab : NavigationTab {
)
)
}
weatherViewModel.state.error != null -> {
AlertDialog(
onDismissRequest = {},
@ -74,6 +78,7 @@ object TodayTab : NavigationTab {
},
)
}
else -> {
Column(
modifier = Modifier
@ -81,15 +86,24 @@ object TodayTab : NavigationTab {
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
WeatherToday(state = weatherViewModel.state)
WeatherCard(
hour = weatherViewModel.state.selected?.let {
weatherViewModel.state.hourlyWeatherInfo?.weatherData?.get(it)
} ?: weatherViewModel.state.hourlyWeatherInfo?.currentWeatherData,
)
WeatherForecast(
state = weatherViewModel.state
) { weatherViewModel.setSelected(it) }
dailyWeatherViewModel.state.dailyWeatherData?.get(0)
?.let { dailyWeatherData ->
WeatherToday(data = dailyWeatherData)
(weatherViewModel.state.selected?.let {
weatherViewModel.state.hourlyWeatherInfo?.weatherData?.get(it)
} ?: weatherViewModel.state.hourlyWeatherInfo?.currentWeatherData)
?.let {
WeatherCard(
hour = it,
dailyData = dailyWeatherData
)
}
WeatherForecast(
state = weatherViewModel.state,
dailyData = dailyWeatherData
) { weatherViewModel.setSelected(it) }
}
}
}
}
@ -98,9 +112,14 @@ object TodayTab : NavigationTab {
@Composable
override fun Actions() {
val viewModel: HourlyWeatherScreenModel = getScreenModel()
val weatherViewModel = getScreenModel<HourlyWeatherScreenModel>()
val dailyWeatherViewModel = getScreenModel<DailyWeatherScreenModel>()
IconButton(onClick = { viewModel.loadWeatherInfo(cache = false) }) {
IconButton(onClick = {
weatherViewModel.loadWeatherInfo(cache = false)
dailyWeatherViewModel.loadWeatherInfo(cache = false)
}) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(R.string.action_reload)

View file

@ -27,7 +27,6 @@ import com.henryhiles.qweather.domain.util.NavigationTab
import com.henryhiles.qweather.presentation.components.weather.WeatherDay
import com.henryhiles.qweather.presentation.components.weather.WeatherToday
import com.henryhiles.qweather.presentation.screenmodel.DailyWeatherScreenModel
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
object WeekTab : NavigationTab {
override val options: TabOptions
@ -47,12 +46,10 @@ object WeekTab : NavigationTab {
@Composable
override fun Content() {
val hourlyWeatherViewModel = getScreenModel<HourlyWeatherScreenModel>()
val dailyWeatherViewModel = getScreenModel<DailyWeatherScreenModel>()
LaunchedEffect(key1 = dailyWeatherViewModel.locationPreferenceManager.selectedIndex) {
dailyWeatherViewModel.loadWeatherInfo()
hourlyWeatherViewModel.loadWeatherInfo()
}
Box(modifier = Modifier.fillMaxSize()) {
@ -83,7 +80,7 @@ object WeekTab : NavigationTab {
else -> {
LazyColumn(contentPadding = PaddingValues(16.dp)) {
dailyWeatherViewModel.state.dailyWeatherData?.let { data ->
item { WeatherToday(state = hourlyWeatherViewModel.state) }
item { WeatherToday(data = data[0]) }
items(data) {
Spacer(modifier = Modifier.height(16.dp))
WeatherDay(dailyWeatherData = it)
@ -97,11 +94,9 @@ object WeekTab : NavigationTab {
@Composable
override fun Actions() {
val hourlyWeatherViewModel = getScreenModel<HourlyWeatherScreenModel>()
val dailyWeatherViewModel = getScreenModel<DailyWeatherScreenModel>()
IconButton(onClick = {
hourlyWeatherViewModel.loadWeatherInfo(cache = false)
dailyWeatherViewModel.loadWeatherInfo(cache = false)
}) {
Icon(

View file

@ -1,4 +1,5 @@
plugins {
kotlin("plugin.serialization") version "1.9.20" apply false
id("com.android.application") version "8.2.0" apply false
id("com.android.library") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false