diff --git a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/model/WeatherForecastData.kt b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/model/WeatherForecastData.kt index 71e0e47..657d657 100644 --- a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/model/WeatherForecastData.kt +++ b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/model/WeatherForecastData.kt @@ -4,27 +4,38 @@ import org.jetbrains.jewel.ui.icon.IconKey import org.jetbrains.plugins.template.weatherApp.ui.WeatherIcons import java.time.LocalDateTime +/** + * Data class representing a daily weather forecast. + */ +internal data class DailyForecast( + val date: LocalDateTime, + val temperature: Float, + val weatherType: WeatherType, + val humidity: Int, + val windSpeed: Float, + val windDirection: WindDirection +) + /** * Data class representing weather information to be displayed in the Weather Card. */ internal data class WeatherForecastData( val location: Location, - val temperature: Float, - val currentTime: LocalDateTime, - val windSpeed: Float, - val windDirection: WindDirection, - val humidity: Int, // Percentage - val weatherType: WeatherType + val currentWeatherForecast: DailyForecast, + val dailyForecasts: List = emptyList() ) { companion object Companion { val EMPTY: WeatherForecastData = WeatherForecastData( Location("", ""), - 0f, - LocalDateTime.now(), - 0f, - WindDirection.NORTH, - 0, - WeatherType.CLEAR + DailyForecast( + LocalDateTime.now(), + 0f, + WeatherType.CLEAR, + 0, + 0f, + WindDirection.NORTH, + ), + emptyList() ) } } diff --git a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/services/WeatherForecastService.kt b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/services/WeatherForecastService.kt index 025f9f1..80786a7 100644 --- a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/services/WeatherForecastService.kt +++ b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/services/WeatherForecastService.kt @@ -8,10 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.jetbrains.plugins.template.weatherApp.model.Location -import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData -import org.jetbrains.plugins.template.weatherApp.model.WeatherType -import org.jetbrains.plugins.template.weatherApp.model.WindDirection +import org.jetbrains.plugins.template.weatherApp.model.* import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -39,23 +36,49 @@ internal class WeatherForecastService(private val cs: CoroutineScope) { * In a real application, this would fetch data from a weather API. */ private suspend fun getWeatherData(location: Location): WeatherForecastData { - val temperature = (-10..40).random().toFloat() - val windSpeed = (0..30).random().toFloat() - val humidity = (30..90).random() + val currentTime = LocalDateTime.of(LocalDate.now(), getRandomTime()) + + // Generate 7-day forecast data + val dailyForecasts = generateDailyForecasts(currentTime) delay(100) return WeatherForecastData( location = location, - temperature = temperature, - currentTime = LocalDateTime.of(LocalDate.now(), getRandomTime()), - windSpeed = windSpeed, - windDirection = WindDirection.random(), - humidity = humidity, - weatherType = WeatherType.random() + dailyForecasts.first(), + dailyForecasts = dailyForecasts.drop(1) ) } + /** + * Generates mock daily forecasts for 7 days starting from the given date. + */ + private fun generateDailyForecasts(startDate: LocalDateTime): List { + val forecasts = mutableListOf() + + for (i in 0 until 8) { + val forecastDate = startDate.plusDays(i.toLong()) + val temperature = (-10..40).random().toFloat() + val windSpeed = (0..30).random().toFloat() + val humidity = (30..90).random() + val weatherType = WeatherType.random() + val windDirection = WindDirection.random() + + forecasts.add( + DailyForecast( + date = forecastDate, + temperature = temperature, + weatherType = weatherType, + humidity = humidity, + windSpeed = windSpeed, + windDirection = windDirection + ) + ) + } + + return forecasts + } + private fun getRandomTime(): LocalTime { val hour = Random.nextInt(0, 24) val minute = Random.nextInt(0, 60) diff --git a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/components/WeatherDetailsCard.kt b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/components/WeatherDetailsCard.kt index 34e52d8..2daa515 100644 --- a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/components/WeatherDetailsCard.kt +++ b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/components/WeatherDetailsCard.kt @@ -2,7 +2,11 @@ package org.jetbrains.plugins.template.weatherApp.ui.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -10,19 +14,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.ActionButton +import org.jetbrains.jewel.ui.component.HorizontallyScrollableContainer import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.icons.AllIconsKeys +import org.jetbrains.plugins.template.weatherApp.WeatherAppColors +import org.jetbrains.plugins.template.weatherApp.model.DailyForecast import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData -import org.jetbrains.plugins.template.weatherApp.model.WeatherType -import org.jetbrains.plugins.template.weatherApp.ui.WeatherIcons import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import kotlin.random.Random +import java.time.format.TextStyle +import java.util.* /** * A composable function that displays a weather card with Jewel theme. @@ -40,8 +47,9 @@ internal fun WeatherDetailsCard( weatherForecastData: WeatherForecastData, onReloadWeatherData: () -> Unit ) { - val isNightTime = isNightTime(weatherForecastData.currentTime) - val cardColor = getCardColorByTemperature(weatherForecastData.temperature, isNightTime) + val currentWeatherForecast = weatherForecastData.currentWeatherForecast + val isNightTime = isNightTime(currentWeatherForecast.date) + val cardColor = getCardColorByTemperature(currentWeatherForecast.temperature, isNightTime) val textColor = Color.White Box( @@ -62,7 +70,7 @@ internal fun WeatherDetailsCard( ) { // Current Time Text( - text = "Time: ${formatDateTime(weatherForecastData.currentTime)}", + text = "Time: ${formatDateTime(currentWeatherForecast.date)}", color = textColor, fontSize = JewelTheme.defaultTextStyle.fontSize, fontWeight = FontWeight.Bold @@ -93,9 +101,11 @@ internal fun WeatherDetailsCard( ) { Icon( - key = WeatherIcons.cloudy, -// key = if (isNightTime) weatherForecastData.weatherType.nightIconKey else weatherForecastData.weatherType.dayIconKey, - contentDescription = weatherForecastData.weatherType.label, + key = when { + isNightTime -> currentWeatherForecast.weatherType.nightIconKey + else -> currentWeatherForecast.weatherType.dayIconKey + }, + contentDescription = currentWeatherForecast.weatherType.label, hint = EmbeddedToInlineCssSvgTransformerHint ) @@ -103,7 +113,7 @@ internal fun WeatherDetailsCard( // Temperature (emphasized) Text( - text = "${weatherForecastData.temperature.toInt()}°C", + text = "${currentWeatherForecast.temperature.toInt()}°C", color = textColor, fontSize = 32.sp, fontWeight = FontWeight.ExtraBold @@ -129,38 +139,171 @@ internal fun WeatherDetailsCard( ) { // Wind info Text( - text = "Wind: ${weatherForecastData.windSpeed.toInt()} km/h ${weatherForecastData.windDirection.label}", + text = "Wind: ${currentWeatherForecast.windSpeed.toInt()} km/h ${currentWeatherForecast.windDirection.label}", color = textColor, fontSize = 16.sp ) // Humidity info Text( - text = "Humidity: ${weatherForecastData.humidity}%", + text = "Humidity: ${currentWeatherForecast.humidity}%", color = textColor, fontSize = 16.sp ) } + + Spacer(modifier = Modifier.height(24.dp)) + + // 7-day forecast section + SevenDaysForecastWidget( + weatherForecastData, + Modifier + .fillMaxWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally), + textColor + ) + } + } +} + +@Composable +private fun SevenDaysForecastWidget( + weatherForecastData: WeatherForecastData, + modifier: Modifier, + textColor: Color +) { + if (weatherForecastData.dailyForecasts.isNotEmpty()) { + Column(modifier) { + Text( + text = "7-Day Forecast", + color = textColor, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + val scrollState = rememberLazyListState() + HorizontallyScrollableContainer( + modifier = Modifier.fillMaxWidth(), + scrollState = scrollState, + ) { + LazyRow( + state = scrollState, + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(weatherForecastData.dailyForecasts) { forecast -> + DayForecastItem( + forecast = forecast, + currentDate = weatherForecastData.currentWeatherForecast.date, + textColor = textColor + ) + } + } + } } } } /** - * Returns a color for the weather type indicator. + * A composable function that displays a single day's forecast. + * + * @param forecast The forecast data for a single day + * @param currentDate The current date for determining relative day names (Today, Tomorrow) + * @param textColor The color of the text */ -fun getWeatherTypeColor(weatherType: WeatherType, baseColor: Color): Color { - return when (weatherType) { - WeatherType.CLEAR -> Color.Yellow.copy(alpha = 0.2f) - WeatherType.CLOUDY -> Color.Gray.copy(alpha = 0.2f) - WeatherType.PARTLY_CLOUDY -> Color.LightGray.copy(alpha = 0.2f) - WeatherType.RAINY_AND_THUNDER, - WeatherType.RAINY -> Color.Blue.copy(alpha = 0.2f) +@Composable +private fun DayForecastItem( + forecast: DailyForecast, + currentDate: LocalDateTime, + textColor: Color +) { + val dayName = getDayName(forecast.date, currentDate) + val date = formatDateTime(forecast.date, showYear = false, showTime = false) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .width(120.dp) + .border(1.dp, textColor.copy(alpha = 0.3f), RoundedCornerShape(8.dp)) + .padding(8.dp) + ) { + // Day name + Text( + text = dayName, + color = textColor, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) - WeatherType.SNOWY -> Color.White.copy(alpha = 0.3f) - WeatherType.TORNADO -> Color.DarkGray.copy(alpha = 0.2f) - WeatherType.THUNDER -> Color.DarkGray.copy(alpha = 0.2f) - WeatherType.FOG -> Color.LightGray.copy(alpha = 0.2f) - WeatherType.MIST -> Color.LightGray.copy(alpha = 0.2f) + Text( + text = date, + color = textColor, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Weather icon + Icon( + key = if (isNightTime(forecast.date)) forecast.weatherType.nightIconKey else forecast.weatherType.dayIconKey, + contentDescription = forecast.weatherType.label, + hint = EmbeddedToInlineCssSvgTransformerHint, + modifier = Modifier.size(48.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Temperature + Text( + text = "${forecast.temperature.toInt()}°C", + color = textColor, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Humidity + Text( + text = "Humidity: ${forecast.humidity}%", + color = textColor, + fontSize = 12.sp + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Wind direction + Text( + text = "Wind: ${forecast.windDirection.label}", + color = textColor, + fontSize = 12.sp + ) + } +} + +/** + * Returns the day name for a given date relative to the current date. + * Returns "Today" for the current date, "Tomorrow" for the next day, + * and the day of week plus date for other days. + */ +private fun getDayName(date: LocalDateTime, currentDate: LocalDateTime): String { + val daysDifference = date.toLocalDate().toEpochDay() - currentDate.toLocalDate().toEpochDay() + + return when (daysDifference) { + 0L -> "Today" + 1L -> "Tomorrow" + else -> { + val dayOfWeek = date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()) + date.dayOfMonth + "$dayOfWeek" + } } } @@ -169,8 +312,8 @@ fun getWeatherTypeColor(weatherType: WeatherType, baseColor: Color): Color { * Night time is considered to be between 7 PM (19:00) and 6 AM (6:00). */ fun isNightTime(dateTime: LocalDateTime): Boolean { - val hour = Random.nextInt(0, 24) - return hour < 6 || hour >= 19 + val hour = dateTime.hour + return hour !in 6..<19 } /** @@ -193,7 +336,17 @@ fun getCardColorByTemperature(temperature: Float, isNightTime: Boolean): Color { /** * Formats the date time to a readable string. */ -fun formatDateTime(dateTime: LocalDateTime): String { - val formatter = DateTimeFormatter.ofPattern("MMM dd, yyyy HH:mm") +fun formatDateTime( + dateTime: LocalDateTime, + showYear: Boolean = true, + showTime: Boolean = true +): String { + val dateFormattingPattern = buildString { + append("dd MMM") + if (showYear) append(" yyyy") + if (showTime) append(", HH:mm") + } + + val formatter = DateTimeFormatter.ofPattern(dateFormattingPattern) return dateTime.format(formatter) } \ No newline at end of file