mirror of
https://github.com/JetBrains/intellij-platform-plugin-template.git
synced 2026-01-22 16:49:22 +00:00
Introduce loading, error and success state for weather forecast loading scenario.
This commit is contained in:
parent
4824b28177
commit
d62e8e9594
@ -1,11 +1,8 @@
|
|||||||
package org.jetbrains.plugins.template.weatherApp.services
|
package org.jetbrains.plugins.template.weatherApp.services
|
||||||
|
|
||||||
import com.intellij.openapi.Disposable
|
import com.intellij.openapi.Disposable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.Location
|
import org.jetbrains.plugins.template.weatherApp.model.Location
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation
|
import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData
|
import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData
|
||||||
@ -33,13 +30,50 @@ interface MyLocationsViewModelApi : Disposable {
|
|||||||
* and user interactions.
|
* and user interactions.
|
||||||
*/
|
*/
|
||||||
interface WeatherViewModelApi : Disposable {
|
interface WeatherViewModelApi : Disposable {
|
||||||
val weatherForecast: Flow<WeatherForecastData>
|
val weatherForecastUIState: Flow<WeatherForecastUIState>
|
||||||
|
|
||||||
fun onLoadWeatherForecast(location: Location)
|
fun onLoadWeatherForecast(location: Location)
|
||||||
|
|
||||||
fun onReloadWeatherForecast()
|
fun onReloadWeatherForecast()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state of a weather data fetching process.
|
||||||
|
*
|
||||||
|
* This class is sealed, meaning it can have a fixed set of subclasses
|
||||||
|
* that represent each possible state of the process. It is designed to
|
||||||
|
* handle and encapsulate the different phases or outcomes when fetching
|
||||||
|
* weather information, such as loading, success, error, or empty state.
|
||||||
|
*
|
||||||
|
* Subclasses:
|
||||||
|
* - `Loading`: Indicates that the weather data is currently being fetched.
|
||||||
|
* - `Success`: Indicates that the weather data was successfully fetched and
|
||||||
|
* contains the loaded data of type `WeatherForecastData`.
|
||||||
|
* - `Error`: Indicates a failure in fetching the weather data, carrying the
|
||||||
|
* error message and optional cause details.
|
||||||
|
* - `Empty`: Indicates that no weather data is available, typically because
|
||||||
|
* no location has been selected.
|
||||||
|
*/
|
||||||
|
sealed class WeatherForecastUIState {
|
||||||
|
data class Loading(val location: Location) : WeatherForecastUIState()
|
||||||
|
data class Success(val weatherForecastData: WeatherForecastData) : WeatherForecastUIState()
|
||||||
|
data class Error(val message: String, val location: Location, val cause: Throwable? = null) :
|
||||||
|
WeatherForecastUIState()
|
||||||
|
|
||||||
|
object Empty : WeatherForecastUIState() // When no location is selected
|
||||||
|
|
||||||
|
fun getLocationOrNull(): Location? {
|
||||||
|
return when (this) {
|
||||||
|
Empty -> null
|
||||||
|
is Error -> location
|
||||||
|
is Loading -> location
|
||||||
|
is Success -> weatherForecastData.location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isLoading: Boolean get() = this is Loading
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ViewModel responsible for managing the user's locations and corresponding weather data.
|
* A ViewModel responsible for managing the user's locations and corresponding weather data.
|
||||||
*
|
*
|
||||||
@ -64,15 +98,19 @@ class WeatherAppViewModel(
|
|||||||
|
|
||||||
private val selectedLocationIndex = MutableStateFlow(myLocations.value.lastIndex)
|
private val selectedLocationIndex = MutableStateFlow(myLocations.value.lastIndex)
|
||||||
|
|
||||||
private val _weatherForecast = MutableStateFlow(WeatherForecastData.EMPTY)
|
private val _weatherState = MutableStateFlow<WeatherForecastUIState>(WeatherForecastUIState.Empty)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stream of weather forecast data that emits updates whenever the forecast changes.
|
* A flow representing the current UI state of the weather forecast.
|
||||||
*
|
*
|
||||||
* This property exposes a Flow of [WeatherForecastData], which allows consumers to observe
|
* This flow emits instances of [WeatherForecastUIState], which encapsulate information
|
||||||
* the weather forecast information for a selected location.
|
* about the state of weather data loading and processing. The emitted states can represent
|
||||||
|
* scenarios such as the data being loaded, successfully fetched, an error occurring, or
|
||||||
|
* the absence of data when no location is selected.
|
||||||
|
*
|
||||||
|
* Observers of this flow can react to these state changes to update the UI accordingly.
|
||||||
*/
|
*/
|
||||||
override val weatherForecast: Flow<WeatherForecastData> = _weatherForecast.asStateFlow()
|
override val weatherForecastUIState: Flow<WeatherForecastUIState> = _weatherState.asStateFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [StateFlow] that emits a list of [SelectableLocation] objects representing the user's
|
* A [StateFlow] that emits a list of [SelectableLocation] objects representing the user's
|
||||||
@ -93,7 +131,7 @@ class WeatherAppViewModel(
|
|||||||
selectedLocationIndex.value = myLocations.value.lastIndex
|
selectedLocationIndex.value = myLocations.value.lastIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_weatherForecast.value.location != locationToAdd) {
|
if (_weatherState.value.getLocationOrNull() != locationToAdd) {
|
||||||
onReloadWeatherForecast()
|
onReloadWeatherForecast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,9 +166,18 @@ class WeatherAppViewModel(
|
|||||||
currentWeatherJob?.cancel()
|
currentWeatherJob?.cancel()
|
||||||
|
|
||||||
currentWeatherJob = viewModelScope.launch {
|
currentWeatherJob = viewModelScope.launch {
|
||||||
val weatherForecastData = weatherService.loadWeatherForecastFor(location).getOrNull() ?: return@launch
|
_weatherState.value = WeatherForecastUIState.Loading(location)
|
||||||
|
|
||||||
_weatherForecast.value = weatherForecastData
|
weatherService.loadWeatherForecastFor(location)
|
||||||
|
.onSuccess { weatherData ->
|
||||||
|
_weatherState.value = WeatherForecastUIState.Success(weatherData)
|
||||||
|
}.onFailure { error ->
|
||||||
|
if (error is CancellationException) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
_weatherState.value = errorStateFor(location, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,4 +190,13 @@ class WeatherAppViewModel(
|
|||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
viewModelScope.cancel()
|
viewModelScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun errorStateFor(
|
||||||
|
location: Location,
|
||||||
|
error: Throwable
|
||||||
|
): WeatherForecastUIState.Error = WeatherForecastUIState.Error(
|
||||||
|
"Failed to load weather forecast for ${location.label}",
|
||||||
|
location,
|
||||||
|
error
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,9 +24,9 @@ import org.jetbrains.jewel.ui.icons.AllIconsKeys
|
|||||||
import org.jetbrains.plugins.template.ComposeTemplateBundle
|
import org.jetbrains.plugins.template.ComposeTemplateBundle
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.Location
|
import org.jetbrains.plugins.template.weatherApp.model.Location
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation
|
import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData
|
|
||||||
import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModelApi
|
import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModelApi
|
||||||
import org.jetbrains.plugins.template.weatherApp.services.SearchAutoCompletionItemProvider
|
import org.jetbrains.plugins.template.weatherApp.services.SearchAutoCompletionItemProvider
|
||||||
|
import org.jetbrains.plugins.template.weatherApp.services.WeatherForecastUIState
|
||||||
import org.jetbrains.plugins.template.weatherApp.services.WeatherViewModelApi
|
import org.jetbrains.plugins.template.weatherApp.services.WeatherViewModelApi
|
||||||
import org.jetbrains.plugins.template.weatherApp.ui.components.SearchToolbarMenu
|
import org.jetbrains.plugins.template.weatherApp.ui.components.SearchToolbarMenu
|
||||||
import org.jetbrains.plugins.template.weatherApp.ui.components.WeatherDetailsCard
|
import org.jetbrains.plugins.template.weatherApp.ui.components.WeatherDetailsCard
|
||||||
@ -201,7 +201,7 @@ private fun RightColumn(
|
|||||||
searchAutoCompletionItemProvider: SearchAutoCompletionItemProvider<Location>,
|
searchAutoCompletionItemProvider: SearchAutoCompletionItemProvider<Location>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val weatherForecastData = weatherViewModelApi.weatherForecast.collectAsState(WeatherForecastData.EMPTY).value
|
val weatherForecastData = weatherViewModelApi.weatherForecastUIState.collectAsState(WeatherForecastUIState.Empty).value
|
||||||
|
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
SearchToolbarMenu(
|
SearchToolbarMenu(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jetbrains.plugins.template.weatherApp.ui.components
|
package org.jetbrains.plugins.template.weatherApp.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
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.border
|
||||||
@ -12,6 +13,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
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.text.style.TextAlign
|
||||||
@ -21,35 +23,53 @@ import org.jetbrains.jewel.foundation.theme.JewelTheme
|
|||||||
import org.jetbrains.jewel.ui.component.*
|
import org.jetbrains.jewel.ui.component.*
|
||||||
import org.jetbrains.jewel.ui.icons.AllIconsKeys
|
import org.jetbrains.jewel.ui.icons.AllIconsKeys
|
||||||
import org.jetbrains.plugins.template.ComposeTemplateBundle
|
import org.jetbrains.plugins.template.ComposeTemplateBundle
|
||||||
|
import org.jetbrains.plugins.template.components.PulsingText
|
||||||
import org.jetbrains.plugins.template.weatherApp.WeatherAppColors
|
import org.jetbrains.plugins.template.weatherApp.WeatherAppColors
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.DailyForecast
|
import org.jetbrains.plugins.template.weatherApp.model.DailyForecast
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.Location
|
import org.jetbrains.plugins.template.weatherApp.model.Location
|
||||||
import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData
|
import org.jetbrains.plugins.template.weatherApp.model.WeatherForecastData
|
||||||
|
import org.jetbrains.plugins.template.weatherApp.services.WeatherForecastUIState
|
||||||
|
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 java.time.format.TextStyle
|
import java.time.format.TextStyle
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A composable function that displays a weather card with Jewel theme.
|
* Displays the weather details in a styled card format. This card presents various weather information
|
||||||
* The card displays city name, temperature, current time, wind information,
|
* including the current time, temperature, city name, wind details, humidity, and a 7-day forecast.
|
||||||
* humidity, and a background icon representing the weather state.
|
* The appearance and content dynamically change based on the given weather state.
|
||||||
* The card and text color change based on temperature and time of day.
|
|
||||||
*
|
*
|
||||||
* @param weatherForecastData The weather data to display
|
* @param modifier Modifier to be applied to the card layout.
|
||||||
* @param modifier Additional modifier for the card
|
* @param weatherForecastState The current state of the weather forecast, which dictates the displayed content
|
||||||
|
* and appearance. It can represent loading, success, error, or empty states.
|
||||||
|
* @param onReloadWeatherData Callback invoked to reload weather data when the refresh action is triggered. It
|
||||||
|
* provides the location for which the weather data should be fetched.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun WeatherDetailsCard(
|
fun WeatherDetailsCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
weatherForecastData: WeatherForecastData,
|
weatherForecastState: WeatherForecastUIState,
|
||||||
onReloadWeatherData: (Location) -> Unit
|
onReloadWeatherData: (Location) -> Unit
|
||||||
) {
|
) {
|
||||||
val currentWeatherForecast = weatherForecastData.currentWeatherForecast
|
|
||||||
val isNightTime = isNightTime(currentWeatherForecast.date)
|
val (cardColor, textColor) = when (weatherForecastState) {
|
||||||
val cardColor = getCardColorByTemperature(currentWeatherForecast.temperature, isNightTime)
|
is WeatherForecastUIState.Success -> {
|
||||||
val textColor = Color.White
|
val isNightTime = isNightTime(weatherForecastState.weatherForecastData.currentWeatherForecast.date)
|
||||||
|
val color =
|
||||||
|
getCardColorByTemperature(
|
||||||
|
weatherForecastState.weatherForecastData.currentWeatherForecast.temperature,
|
||||||
|
isNightTime
|
||||||
|
)
|
||||||
|
color to Color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
is WeatherForecastUIState.Loading -> WeatherAppColors.mildWeatherColor to Color.White
|
||||||
|
is WeatherForecastUIState.Error -> WeatherAppColors.hotWeatherColor to Color.White // Brown for errors
|
||||||
|
is WeatherForecastUIState.Empty -> WeatherAppColors.coolWeatherColor to Color.White
|
||||||
|
}
|
||||||
|
|
||||||
VerticallyScrollableContainer(modifier = modifier.safeContentPadding()) {
|
VerticallyScrollableContainer(modifier = modifier.safeContentPadding()) {
|
||||||
Box(
|
Box(
|
||||||
@ -69,42 +89,18 @@ fun WeatherDetailsCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
// Current Time
|
// Current Time
|
||||||
/**
|
TimeDisplay(weatherForecastState, textColor)
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
|
||||||
text = ComposeTemplateBundle.message(
|
|
||||||
"weather.app.time.text",
|
|
||||||
formatDateTime(currentWeatherForecast.date)
|
|
||||||
),
|
|
||||||
color = textColor,
|
|
||||||
fontSize = JewelTheme.defaultTextStyle.fontSize,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.ActionButton
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Button.kt
|
|
||||||
*/
|
|
||||||
ActionButton(
|
ActionButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(Color.Transparent)
|
.background(Color.Transparent)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
tooltip = {
|
tooltip = { Text("Refresh weather data") },
|
||||||
/**
|
onClick = {
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
weatherForecastState.getLocationOrNull()?.let { onReloadWeatherData(it) }
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text("Refresh weather data")
|
|
||||||
},
|
},
|
||||||
onClick = { onReloadWeatherData(weatherForecastData.location) },
|
|
||||||
) {
|
) {
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Icon
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Icon.kt
|
|
||||||
*/
|
|
||||||
Icon(
|
Icon(
|
||||||
key = AllIconsKeys.Actions.Refresh,
|
key = AllIconsKeys.Actions.Refresh,
|
||||||
contentDescription = "Refresh",
|
contentDescription = "Refresh",
|
||||||
@ -121,98 +117,33 @@ fun WeatherDetailsCard(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
WeatherIconDisplay(weatherForecastState)
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Icon
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Icon.kt
|
|
||||||
*/
|
|
||||||
Icon(
|
|
||||||
key = when {
|
|
||||||
isNightTime -> currentWeatherForecast.weatherType.nightIconKey
|
|
||||||
else -> currentWeatherForecast.weatherType.dayIconKey
|
|
||||||
},
|
|
||||||
contentDescription = currentWeatherForecast.weatherType.label,
|
|
||||||
hint = EmbeddedToInlineCssSvgTransformerHint
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Temperature (emphasized)
|
TemperatureDisplay(weatherForecastState, textColor)
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
|
||||||
text = ComposeTemplateBundle.message(
|
|
||||||
"weather.app.temperature.text",
|
|
||||||
currentWeatherForecast.temperature.toInt()
|
|
||||||
),
|
|
||||||
color = textColor,
|
|
||||||
fontSize = 32.sp,
|
|
||||||
fontWeight = FontWeight.ExtraBold
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// City name
|
// City name
|
||||||
/**
|
CityNameDisplay(weatherForecastState, textColor)
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
|
||||||
text = weatherForecastData.location.label,
|
|
||||||
color = textColor,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Wind and humidity info
|
// Wind and humidity info
|
||||||
Row(
|
WeatherDetailsRow(Modifier.fillMaxWidth(), weatherForecastState, textColor)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
// Wind info
|
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
|
||||||
text = ComposeTemplateBundle.message(
|
|
||||||
"weather.app.wind.direction.text",
|
|
||||||
currentWeatherForecast.windSpeed.toInt(),
|
|
||||||
currentWeatherForecast.windDirection.label
|
|
||||||
),
|
|
||||||
color = textColor,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Humidity info
|
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
|
||||||
text = ComposeTemplateBundle.message(
|
|
||||||
"weather.app.humidity.text",
|
|
||||||
currentWeatherForecast.humidity
|
|
||||||
),
|
|
||||||
color = textColor,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// 7-day forecast section
|
// 7-day forecast section
|
||||||
SevenDaysForecastWidget(
|
SevenDaysForecastWidget(
|
||||||
weatherForecastData,
|
weatherForecastState,
|
||||||
|
textColor,
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.align(Alignment.CenterHorizontally),
|
.align(Alignment.CenterHorizontally)
|
||||||
textColor
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -227,10 +158,6 @@ private fun SevenDaysForecastWidget(
|
|||||||
) {
|
) {
|
||||||
if (weatherForecastData.dailyForecasts.isNotEmpty()) {
|
if (weatherForecastData.dailyForecasts.isNotEmpty()) {
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
Text(
|
||||||
text = ComposeTemplateBundle.message("weather.app.7days.forecast.title.text"),
|
text = ComposeTemplateBundle.message("weather.app.7days.forecast.title.text"),
|
||||||
color = textColor,
|
color = textColor,
|
||||||
@ -242,10 +169,7 @@ private fun SevenDaysForecastWidget(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
val scrollState = rememberLazyListState()
|
val scrollState = rememberLazyListState()
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.HorizontallyScrollableContainer
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ScrollableContainer.kt
|
|
||||||
*/
|
|
||||||
HorizontallyScrollableContainer(
|
HorizontallyScrollableContainer(
|
||||||
modifier = Modifier.fillMaxWidth().safeContentPadding(),
|
modifier = Modifier.fillMaxWidth().safeContentPadding(),
|
||||||
scrollState = scrollState,
|
scrollState = scrollState,
|
||||||
@ -292,10 +216,6 @@ private fun DayForecastItem(
|
|||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
// Day name
|
// Day name
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
Text(
|
||||||
text = dayName,
|
text = dayName,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
@ -304,10 +224,6 @@ private fun DayForecastItem(
|
|||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
Text(
|
||||||
text = date,
|
text = date,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
@ -319,10 +235,6 @@ private fun DayForecastItem(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Weather icon
|
// Weather icon
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Icon
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Icon.kt
|
|
||||||
*/
|
|
||||||
Icon(
|
Icon(
|
||||||
key = if (isNightTime(forecast.date)) forecast.weatherType.nightIconKey else forecast.weatherType.dayIconKey,
|
key = if (isNightTime(forecast.date)) forecast.weatherType.nightIconKey else forecast.weatherType.dayIconKey,
|
||||||
contentDescription = forecast.weatherType.label,
|
contentDescription = forecast.weatherType.label,
|
||||||
@ -333,10 +245,6 @@ private fun DayForecastItem(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Temperature
|
// Temperature
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
Text(
|
||||||
text = ComposeTemplateBundle.message(
|
text = ComposeTemplateBundle.message(
|
||||||
"weather.app.temperature.text",
|
"weather.app.temperature.text",
|
||||||
@ -350,10 +258,6 @@ private fun DayForecastItem(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Humidity
|
// Humidity
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
Text(
|
||||||
text = ComposeTemplateBundle.message(
|
text = ComposeTemplateBundle.message(
|
||||||
"weather.app.humidity.text",
|
"weather.app.humidity.text",
|
||||||
@ -366,10 +270,6 @@ private fun DayForecastItem(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Wind direction
|
// Wind direction
|
||||||
/**
|
|
||||||
* Jewel org.jetbrains.jewel.ui.component.Text
|
|
||||||
* @see https://github.com/JetBrains/intellij-community/blob/master/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Text.kt
|
|
||||||
*/
|
|
||||||
Text(
|
Text(
|
||||||
text = ComposeTemplateBundle.message(
|
text = ComposeTemplateBundle.message(
|
||||||
"weather.app.wind.direction.text",
|
"weather.app.wind.direction.text",
|
||||||
@ -382,6 +282,327 @@ private fun DayForecastItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time display component with loading state
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun TimeDisplay(
|
||||||
|
weatherState: WeatherForecastUIState,
|
||||||
|
textColor: Color
|
||||||
|
) {
|
||||||
|
val text = when (weatherState) {
|
||||||
|
is WeatherForecastUIState.Success -> formatDateTime(weatherState.weatherForecastData.currentWeatherForecast.date)
|
||||||
|
else -> "-"
|
||||||
|
}.let { time -> ComposeTemplateBundle.message("weather.app.time.text", time) }
|
||||||
|
|
||||||
|
PulsingText(
|
||||||
|
text,
|
||||||
|
weatherState.isLoading,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = JewelTheme.defaultTextStyle.fontSize,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather icon that shows spinning progress during loading
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun WeatherIconDisplay(
|
||||||
|
weatherState: WeatherForecastUIState,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
when (weatherState) {
|
||||||
|
is WeatherForecastUIState.Loading -> {
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "rotating_weather_icon")
|
||||||
|
|
||||||
|
val rotation = infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 360f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 3000, // 3 seconds per rotation
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "icon_rotation"
|
||||||
|
).value
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
key = WeatherIcons.dayClear,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier.rotate(rotation),
|
||||||
|
hint = EmbeddedToInlineCssSvgTransformerHint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is WeatherForecastUIState.Success -> {
|
||||||
|
val currentForecast = weatherState.weatherForecastData.currentWeatherForecast
|
||||||
|
val isNightTime = isNightTime(currentForecast.date)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
key = if (isNightTime) {
|
||||||
|
currentForecast.weatherType.nightIconKey
|
||||||
|
} else {
|
||||||
|
currentForecast.weatherType.dayIconKey
|
||||||
|
},
|
||||||
|
contentDescription = currentForecast.weatherType.label,
|
||||||
|
hint = EmbeddedToInlineCssSvgTransformerHint,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is WeatherForecastUIState.Error -> {
|
||||||
|
Icon(
|
||||||
|
key = AllIconsKeys.General.Warning,
|
||||||
|
contentDescription = "Weather data error",
|
||||||
|
tint = Color.White.copy(alpha = 0.8f),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is WeatherForecastUIState.Empty -> {
|
||||||
|
Icon(
|
||||||
|
key = AllIconsKeys.Actions.Find,
|
||||||
|
contentDescription = "No location selected",
|
||||||
|
tint = Color.White.copy(alpha = 0.6f),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temperature display with loading animation
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun TemperatureDisplay(
|
||||||
|
weatherState: WeatherForecastUIState,
|
||||||
|
textColor: Color
|
||||||
|
) {
|
||||||
|
val temperatureText = when (weatherState) {
|
||||||
|
is WeatherForecastUIState.Success -> ComposeTemplateBundle.message(
|
||||||
|
"weather.app.temperature.text",
|
||||||
|
weatherState.weatherForecastData.currentWeatherForecast.temperature.toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
is WeatherForecastUIState.Loading -> "--°"
|
||||||
|
is WeatherForecastUIState.Error -> "N/A°"
|
||||||
|
is WeatherForecastUIState.Empty -> "--°"
|
||||||
|
}
|
||||||
|
|
||||||
|
PulsingText(
|
||||||
|
text = temperatureText,
|
||||||
|
isLoading = weatherState.isLoading,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* City name display that shows "Loading..." during loading state
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun CityNameDisplay(
|
||||||
|
weatherState: WeatherForecastUIState,
|
||||||
|
textColor: Color
|
||||||
|
) {
|
||||||
|
val loadingText = when (weatherState) {
|
||||||
|
is WeatherForecastUIState.Success -> weatherState.weatherForecastData.location.label
|
||||||
|
is WeatherForecastUIState.Loading -> weatherState.location.label
|
||||||
|
is WeatherForecastUIState.Error -> "weatherState.location.label} - Error"
|
||||||
|
is WeatherForecastUIState.Empty -> "Select a location"
|
||||||
|
}
|
||||||
|
|
||||||
|
PulsingText(
|
||||||
|
text = loadingText,
|
||||||
|
isLoading = weatherState.isLoading,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable function to display a row of weather details including wind and humidity information.
|
||||||
|
*
|
||||||
|
* @param modifier A [Modifier] that can be used to customize the layout or add behavior to the composable.
|
||||||
|
* @param weatherState The current state of the weather forecast, represented by [WeatherForecastUIState].
|
||||||
|
* This determines the display of wind and humidity information based on state.
|
||||||
|
* @param textColor The color to be applied to the text of the weather details.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun WeatherDetailsRow(
|
||||||
|
modifier: Modifier,
|
||||||
|
weatherState: WeatherForecastUIState,
|
||||||
|
textColor: Color
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
// Wind info
|
||||||
|
val windText = when (weatherState) {
|
||||||
|
is WeatherForecastUIState.Success -> {
|
||||||
|
val forecast = weatherState.weatherForecastData.currentWeatherForecast
|
||||||
|
ComposeTemplateBundle.message(
|
||||||
|
"weather.app.wind.direction.text",
|
||||||
|
forecast.windSpeed.toInt(),
|
||||||
|
forecast.windDirection.label
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is WeatherForecastUIState.Loading -> "Wind: --"
|
||||||
|
is WeatherForecastUIState.Error -> "Wind: N/A"
|
||||||
|
is WeatherForecastUIState.Empty -> "Wind: --"
|
||||||
|
}
|
||||||
|
|
||||||
|
PulsingText(
|
||||||
|
windText,
|
||||||
|
weatherState.isLoading,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
// Humidity info
|
||||||
|
val humidityText = when (weatherState) {
|
||||||
|
is WeatherForecastUIState.Success -> ComposeTemplateBundle.message(
|
||||||
|
"weather.app.humidity.text",
|
||||||
|
weatherState.weatherForecastData.currentWeatherForecast.humidity
|
||||||
|
)
|
||||||
|
|
||||||
|
is WeatherForecastUIState.Loading -> "Humidity: -- %"
|
||||||
|
is WeatherForecastUIState.Error -> "Humidity: N/A"
|
||||||
|
is WeatherForecastUIState.Empty -> "Humidity: -- %"
|
||||||
|
}
|
||||||
|
PulsingText(
|
||||||
|
text = humidityText,
|
||||||
|
weatherState.isLoading,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forecast section that shows skeleton during loading
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun SevenDaysForecastWidget(
|
||||||
|
weatherState: WeatherForecastUIState,
|
||||||
|
textColor: Color,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
when (weatherState) {
|
||||||
|
is WeatherForecastUIState.Success -> {
|
||||||
|
if (weatherState.weatherForecastData.dailyForecasts.isNotEmpty()) {
|
||||||
|
SevenDaysForecastWidget(
|
||||||
|
weatherState.weatherForecastData,
|
||||||
|
modifier,
|
||||||
|
textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is WeatherForecastUIState.Loading -> LoadingForecastSkeleton(textColor)
|
||||||
|
is WeatherForecastUIState.Error -> ErrorForecastMessage(textColor)
|
||||||
|
is WeatherForecastUIState.Empty -> EmptyForecastMessage(textColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading skeleton for forecast section
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun LoadingForecastSkeleton(textColor: Color) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = ComposeTemplateBundle.message("weather.app.7days.forecast.title.text"),
|
||||||
|
color = textColor.copy(alpha = 0.7f),
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
val scrollState = rememberLazyListState()
|
||||||
|
HorizontallyScrollableContainer(
|
||||||
|
modifier = Modifier.fillMaxWidth().safeContentPadding(),
|
||||||
|
scrollState = scrollState,
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
state = scrollState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(count = 7) { LoadingForecastItem(textColor) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoadingForecastItem(textColor: Color) {
|
||||||
|
val infiniteTransition = rememberInfiniteTransition()
|
||||||
|
val alpha = infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0.3f,
|
||||||
|
targetValue = 0.6f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
)
|
||||||
|
).value
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(120.dp)
|
||||||
|
.border(1.dp, textColor.copy(alpha = 0.3f), RoundedCornerShape(8.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text("--", color = textColor.copy(alpha = alpha), fontSize = 14.sp)
|
||||||
|
Text("", color = textColor.copy(alpha = alpha), fontSize = 14.sp)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(textColor.copy(alpha = alpha), RoundedCornerShape(4.dp))
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text("--°", color = textColor.copy(alpha = alpha), fontSize = 16.sp)
|
||||||
|
Text("", color = textColor.copy(alpha = alpha), fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ErrorForecastMessage(textColor: Color) {
|
||||||
|
Text(
|
||||||
|
text = "Forecast unavailable",
|
||||||
|
color = textColor.copy(alpha = 0.7f),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyForecastMessage(textColor: Color) {
|
||||||
|
Text(
|
||||||
|
text = "Select a location to view forecast",
|
||||||
|
color = textColor.copy(alpha = 0.7f),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the day name for a given date relative to the current date.
|
* Returns the day name for a given date relative to the current date.
|
||||||
* Returns "Today" for the current date, "Tomorrow" for the next day,
|
* Returns "Today" for the current date, "Tomorrow" for the next day,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user