Add a 7-Days forecast widget

This commit is contained in:
Nebojsa Vuksic 2025-07-29 15:25:33 +02:00
parent b800d6bb17
commit 01d981038c
3 changed files with 241 additions and 54 deletions

View File

@ -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<DailyForecast> = 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()
)
}
}

View File

@ -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<DailyForecast> {
val forecasts = mutableListOf<DailyForecast>()
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)

View File

@ -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)
}