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 org.jetbrains.plugins.template.weatherApp.ui.WeatherIcons
import java.time.LocalDateTime 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. * Data class representing weather information to be displayed in the Weather Card.
*/ */
internal data class WeatherForecastData( internal data class WeatherForecastData(
val location: Location, val location: Location,
val temperature: Float, val currentWeatherForecast: DailyForecast,
val currentTime: LocalDateTime, val dailyForecasts: List<DailyForecast> = emptyList()
val windSpeed: Float,
val windDirection: WindDirection,
val humidity: Int, // Percentage
val weatherType: WeatherType
) { ) {
companion object Companion { companion object Companion {
val EMPTY: WeatherForecastData = WeatherForecastData( val EMPTY: WeatherForecastData = WeatherForecastData(
Location("", ""), Location("", ""),
0f, DailyForecast(
LocalDateTime.now(), LocalDateTime.now(),
0f, 0f,
WindDirection.NORTH, WeatherType.CLEAR,
0, 0,
WeatherType.CLEAR 0f,
WindDirection.NORTH,
),
emptyList()
) )
} }
} }

View File

@ -8,10 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.plugins.template.weatherApp.model.Location import org.jetbrains.plugins.template.weatherApp.model.*
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 java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime 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. * In a real application, this would fetch data from a weather API.
*/ */
private suspend fun getWeatherData(location: Location): WeatherForecastData { private suspend fun getWeatherData(location: Location): WeatherForecastData {
val temperature = (-10..40).random().toFloat() val currentTime = LocalDateTime.of(LocalDate.now(), getRandomTime())
val windSpeed = (0..30).random().toFloat()
val humidity = (30..90).random() // Generate 7-day forecast data
val dailyForecasts = generateDailyForecasts(currentTime)
delay(100) delay(100)
return WeatherForecastData( return WeatherForecastData(
location = location, location = location,
temperature = temperature, dailyForecasts.first(),
currentTime = LocalDateTime.of(LocalDate.now(), getRandomTime()), dailyForecasts = dailyForecasts.drop(1)
windSpeed = windSpeed,
windDirection = WindDirection.random(),
humidity = humidity,
weatherType = WeatherType.random()
) )
} }
/**
* 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 { private fun getRandomTime(): LocalTime {
val hour = Random.nextInt(0, 24) val hour = Random.nextInt(0, 24)
val minute = Random.nextInt(0, 60) 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.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* 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.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -10,19 +14,22 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.ActionButton 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.Icon
import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.icons.AllIconsKeys 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.WeatherForecastData
import org.jetbrains.plugins.template.weatherApp.model.WeatherType
import org.jetbrains.plugins.template.weatherApp.ui.WeatherIcons
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter 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. * A composable function that displays a weather card with Jewel theme.
@ -40,8 +47,9 @@ internal fun WeatherDetailsCard(
weatherForecastData: WeatherForecastData, weatherForecastData: WeatherForecastData,
onReloadWeatherData: () -> Unit onReloadWeatherData: () -> Unit
) { ) {
val isNightTime = isNightTime(weatherForecastData.currentTime) val currentWeatherForecast = weatherForecastData.currentWeatherForecast
val cardColor = getCardColorByTemperature(weatherForecastData.temperature, isNightTime) val isNightTime = isNightTime(currentWeatherForecast.date)
val cardColor = getCardColorByTemperature(currentWeatherForecast.temperature, isNightTime)
val textColor = Color.White val textColor = Color.White
Box( Box(
@ -62,7 +70,7 @@ internal fun WeatherDetailsCard(
) { ) {
// Current Time // Current Time
Text( Text(
text = "Time: ${formatDateTime(weatherForecastData.currentTime)}", text = "Time: ${formatDateTime(currentWeatherForecast.date)}",
color = textColor, color = textColor,
fontSize = JewelTheme.defaultTextStyle.fontSize, fontSize = JewelTheme.defaultTextStyle.fontSize,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@ -93,9 +101,11 @@ internal fun WeatherDetailsCard(
) { ) {
Icon( Icon(
key = WeatherIcons.cloudy, key = when {
// key = if (isNightTime) weatherForecastData.weatherType.nightIconKey else weatherForecastData.weatherType.dayIconKey, isNightTime -> currentWeatherForecast.weatherType.nightIconKey
contentDescription = weatherForecastData.weatherType.label, else -> currentWeatherForecast.weatherType.dayIconKey
},
contentDescription = currentWeatherForecast.weatherType.label,
hint = EmbeddedToInlineCssSvgTransformerHint hint = EmbeddedToInlineCssSvgTransformerHint
) )
@ -103,7 +113,7 @@ internal fun WeatherDetailsCard(
// Temperature (emphasized) // Temperature (emphasized)
Text( Text(
text = "${weatherForecastData.temperature.toInt()}°C", text = "${currentWeatherForecast.temperature.toInt()}°C",
color = textColor, color = textColor,
fontSize = 32.sp, fontSize = 32.sp,
fontWeight = FontWeight.ExtraBold fontWeight = FontWeight.ExtraBold
@ -129,38 +139,171 @@ internal fun WeatherDetailsCard(
) { ) {
// Wind info // Wind info
Text( Text(
text = "Wind: ${weatherForecastData.windSpeed.toInt()} km/h ${weatherForecastData.windDirection.label}", text = "Wind: ${currentWeatherForecast.windSpeed.toInt()} km/h ${currentWeatherForecast.windDirection.label}",
color = textColor, color = textColor,
fontSize = 16.sp fontSize = 16.sp
) )
// Humidity info // Humidity info
Text( Text(
text = "Humidity: ${weatherForecastData.humidity}%", text = "Humidity: ${currentWeatherForecast.humidity}%",
color = textColor, color = textColor,
fontSize = 16.sp 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 { @Composable
return when (weatherType) { private fun DayForecastItem(
WeatherType.CLEAR -> Color.Yellow.copy(alpha = 0.2f) forecast: DailyForecast,
WeatherType.CLOUDY -> Color.Gray.copy(alpha = 0.2f) currentDate: LocalDateTime,
WeatherType.PARTLY_CLOUDY -> Color.LightGray.copy(alpha = 0.2f) textColor: Color
WeatherType.RAINY_AND_THUNDER, ) {
WeatherType.RAINY -> Color.Blue.copy(alpha = 0.2f) 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) Text(
WeatherType.TORNADO -> Color.DarkGray.copy(alpha = 0.2f) text = date,
WeatherType.THUNDER -> Color.DarkGray.copy(alpha = 0.2f) color = textColor,
WeatherType.FOG -> Color.LightGray.copy(alpha = 0.2f) fontSize = 10.sp,
WeatherType.MIST -> Color.LightGray.copy(alpha = 0.2f) 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). * Night time is considered to be between 7 PM (19:00) and 6 AM (6:00).
*/ */
fun isNightTime(dateTime: LocalDateTime): Boolean { fun isNightTime(dateTime: LocalDateTime): Boolean {
val hour = Random.nextInt(0, 24) val hour = dateTime.hour
return hour < 6 || hour >= 19 return hour !in 6..<19
} }
/** /**
@ -193,7 +336,17 @@ fun getCardColorByTemperature(temperature: Float, isNightTime: Boolean): Color {
/** /**
* Formats the date time to a readable string. * Formats the date time to a readable string.
*/ */
fun formatDateTime(dateTime: LocalDateTime): String { fun formatDateTime(
val formatter = DateTimeFormatter.ofPattern("MMM dd, yyyy HH:mm") 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) return dateTime.format(formatter)
} }