From e4c64b8bb9bf28bc0f498f64b231d617f5d76936 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Fri, 24 Jul 2026 20:46:04 +0530 Subject: [PATCH 01/40] comment out support tile in SettingsScreen and restore startDestination to NavRoutes.MAIN --- .../core/ui/navigation/AppNavHost.kt | 4 ++-- .../feature/settings/SettingsScreen.kt | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt index 63abc8189..5e8ceae51 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt @@ -67,8 +67,8 @@ fun AppNavHost( ) { navigation( route = "root", -// startDestination = NavRoutes.MAIN - startDestination = NavRoutes.editLocation("af673c89-72b7-4da9-865d-c26fdabeca07") + startDestination = NavRoutes.MAIN +// startDestination = NavRoutes.editLocation("af673c89-72b7-4da9-865d-c26fdabeca07") ) { composable( NavRoutes.MAIN diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/settings/SettingsScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/settings/SettingsScreen.kt index 6b5296822..6a62f6f22 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/settings/SettingsScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/settings/SettingsScreen.kt @@ -47,12 +47,12 @@ fun SettingsScreen(navController: NavController) { description = stringResource(R.string.setting_appearance_secondary), onClick = { navController.navigate(NavRoutes.APPEARANCE) } ), - SettingTile.ActionTile( - leading = { SettingsTileIcon(R.drawable.favorite_24px) }, - title = "Support", - description = "If you enjoy the app, consider supporting its development on Github", - onClick = { uriHandler.openUri("https://github.com/sponsors/PranshulGG?frequency=one-time&sponsor=PranshulGG") } - ) +// SettingTile.ActionTile( +// leading = { SettingsTileIcon(R.drawable.favorite_24px) }, +// title = "Support", +// description = "If you enjoy the app, consider supporting its development", +// onClick = { uriHandler.openUri("https://github.com/sponsors/PranshulGG?frequency=one-time&sponsor=PranshulGG") } +// ) ) ) From 8bd8fb70a33bf76ee4c30d321cf27883d5dda66a Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:26:57 +0530 Subject: [PATCH 02/40] add location_long_press_info string and update multiple localizations --- app/src/main/res/values-de-rDE/strings.xml | 5 ++--- app/src/main/res/values-es-rES/strings.xml | 5 ++--- app/src/main/res/values-fi-rFI/strings.xml | 5 ++--- app/src/main/res/values-fr-rFR/strings.xml | 5 ++--- app/src/main/res/values-ja-rJP/strings.xml | 5 ++--- app/src/main/res/values-pt-rBR/strings.xml | 5 ++--- app/src/main/res/values-zh-rCN/strings.xml | 7 +++---- app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 17 insertions(+), 22 deletions(-) diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 26cab0789..af5755d95 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -279,7 +279,7 @@ Von %1$s %1$s Std Keine Ergebnisse gefunden - + It will stay %1$s throughout the day and night. Am Tag wird es %1$s sein und in der Nacht %2$s werden. Tagsüber wird gegen %1$s Regen erwartet, mit einer Wahrscheinlichkeit von %2$s. Heute Nacht besteht gegen %4$s eine Regenwahrscheinlichkeit von %3$s. Tagsüber wird gegen %1$s Regen erwartet, mit einer Wahrscheinlichkeit von %2$s. @@ -314,5 +314,4 @@ Air quality source Location name Location information - - + diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 20a1feccb..3bdfeccc1 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -284,7 +284,7 @@ Durante el día, se espera lluvia alrededor de las %1$s con una probabilidad del %2$s. Esta noche, hay una probabilidad del %3$s que llueva alrededor de las %4$s Durante el día, se espera lluvia alrededor de las %1$s con una probabilidad de %2$s. Esta noche, hay una probabilidad del %1$s que llueva alrededor de las %2$s. - + Snow is forecast around %1$s during the day with a %2$s chance. Overnight, snowfall remains possible with a %3$s chance around %4$s. Snow is forecast around %1$s during the day with a %2$s chance. Overnight, there\'s a %1$s chance of snow around %2$s. Expect extreme heat during the day with a high of %1$s. Temperatures will ease to around %2$s tonight. @@ -314,5 +314,4 @@ Air quality source Location name Location information - - + diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 95628439b..22f06e850 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -281,7 +281,7 @@ %1$s tuntia Tuloksia ei löytynyt Luvassa %1$s päivällä ja yöllä. - Päivällä luvassa poutaa, yöksi %2$s. + Päivällä on %1$s, yöllä muuttuu %2$s. Päivällä on luvassa sadetta noin kello %1$s %2$s todennäköisyydellä. Yöllä on %3$s todennäköisyys sateeseen noin kello %4$s. Päivällä on luvassa sadetta noin kello %1$s %2$s todennäköisyydellä. Yöllä on %1$s todennäköisyys sateeseen noin kello %2$s. @@ -315,5 +315,4 @@ Air quality source Location name Location information - - + diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 38b3cd716..3b2d436de 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -280,7 +280,7 @@ %1$s h Aucun résultat trouvé Le ciel restera %1$s tout au long de la journée et de la nuit. - Il fera %1$s pendant la journée, puis %1$s la nuit. + Il fera %1$s pendant la journée, puis %2$s la nuit. En journée, de la pluie est attendue vers %1$s avec %2$s de chance. Ce soir, il y a %3$s de chance de pluie vers %4$s. En journée, de la pluie est attendue vers %1$s avec %2$s de chance. Ce soir, il y a %1$s de chance de pluie vers %2$s. @@ -314,5 +314,4 @@ Source des données de qualité de l\'air Nom de l’emplacement Informations sur l\'emplacement - - + diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 71a685027..030f4df47 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -279,7 +279,7 @@ 一日を通して%1$sの状態が続くでしょう。 日中は%1$sですが、夜からは%2$sになる見込みです。 日中は%1$s頃に降水確率%2$sで雨が予想されています。今夜は%4$s頃に降水確率%3$sで雨が降る見込みです。 - 日中は%1$s頃に降水確率\%2$sで雨が予想されています。 + 日中は%1$s頃に降水確率%2$sで雨が予想されています。 今夜は%2$s頃に降水確率%1$sで雨が降る見込みです。 日中は%1$s頃に降雪確率%2$sで雪が予想されています。夜間は%4$s頃に降雪確率%3$sで引き続き雪が降る可能性があります。 日中は%1$s頃に降雪確率%2$sで雪が予想されています。 @@ -311,5 +311,4 @@ Air quality source Location name Location information - - + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0931975ed..fc86bd0d1 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -283,7 +283,7 @@ São emitidas por veículos motorizados, aquecedores a lenha e indústrias. Inc Continuará %1$s ao longo do dia e da noite. Fará %1$s durante o dia e mudará para %2$s à noite. Durante o dia, espera-se chuva por volta das %1$s com uma chance de \%2$s. Hoje à noite, há uma chance de %3$s de chuva por volta das \%4$s. - + During the day, rain is expected around %1$s with a %2$s chance. Esta noite, há %1$s de probabilidade de chuva por volta das %2$s. Há previsão de neve por volta das %1$s durante o dia, com %2$s de probabilidade. Durante a noite, ainda há possibilidade de neve, com %3$s de probabilidade por volta das %4$s. Há previsão de neve por volta das %1$s durante o dia, com %2$s de probabilidade. @@ -315,5 +315,4 @@ São emitidas por veículos motorizados, aquecedores a lenha e indústrias. Inc Air quality source Location name Location information - - + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a45eb87d8..07e1549fa 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -280,7 +280,7 @@ %1$s风 %1$s 小时 未找到结果 - + It will stay %1$s throughout the day and night 日间将以%1$s为主,夜间转为%2$s。 During the day, rain is expected around %1$s with a %2$s chance. Tonight, there\'s a %3$s chance of rain around %4$s. During the day, rain is expected around %1$s with a %2$s chance. @@ -296,7 +296,7 @@ 白天%1$s。 Conditions will turn %1$s at night. Gadget bridge is not installed - + Worker information Send data to Gadgetbridge Particulate matter less than 2.5 microns Particulate matter less than 10 microns @@ -315,5 +315,4 @@ Air quality source Location name Location information - - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d472f9672..eab37fa6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -381,6 +381,8 @@ Air quality source Location name Location information + Long press a location for more options + From 505c7c53b43e6b01c71292bbbc104672a396ea74 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:27:31 +0530 Subject: [PATCH 03/40] Implement AccuWeather Air Quality support --- .../core/model/sources/AirQualitySource.kt | 2 +- .../weather/airquality/accu/AccuCityKey.kt | 7 ++ .../sources/airquality/accu/AccuAqiApi.kt | 68 +++++++++++++ .../airquality/accu/AccuAqiRepository.kt | 95 +++++++++++++++++++ .../airquality/accu/json/AccuAqiJson.kt | 35 +++++++ .../accu/json/bundle/AccuAqiJsonBundle.kt | 9 ++ .../sources/weather/accu/AccuRepository.kt | 24 +++-- .../weather/cache/AirQualityCachePolicy.kt | 7 +- .../local/dao/airquality/AirQualityDao.kt | 4 +- .../data/local/dao/airquality/accu/AccuDao.kt | 22 +++++ .../entity/airquality/accu/AccuEntity.kt | 24 +++++ .../airquality/sources/accu/AccuAqiMapper.kt | 41 ++++++++ .../weather/sources/accu/AccuEntityMapper.kt | 23 +++++ .../provider/AirQualityRepositoryProvider.kt | 23 +++++ .../data/repository/AirQualityRepository.kt | 15 +++ 15 files changed, 388 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/airquality/accu/AccuCityKey.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/AccuAqiApi.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/AccuAqiRepository.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/json/AccuAqiJson.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/json/bundle/AccuAqiJsonBundle.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/airquality/accu/AccuDao.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/airquality/accu/AccuEntity.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/accu/AccuAqiMapper.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/accu/AccuEntityMapper.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AirQualityRepositoryProvider.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AirQualityRepository.kt diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/sources/AirQualitySource.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/sources/AirQualitySource.kt index 32f4bac4b..6a35f83ed 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/sources/AirQualitySource.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/sources/AirQualitySource.kt @@ -16,6 +16,6 @@ enum class AirQualitySource( displayName = "Open Meteo", fullName = "Open Meteo", displayLink = "https://open-meteo.com/" - ), + ) } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/airquality/accu/AccuCityKey.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/airquality/accu/AccuCityKey.kt new file mode 100644 index 000000000..5d613fb11 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/airquality/accu/AccuCityKey.kt @@ -0,0 +1,7 @@ +package com.pranshulgg.weather_master_app.core.model.weather.airquality.accu + + +data class AccuCityKey( + val locationId: String, + val cityKey: String +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/AccuAqiApi.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/AccuAqiApi.kt new file mode 100644 index 000000000..d1fa4e27a --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/AccuAqiApi.kt @@ -0,0 +1,68 @@ +package com.pranshulgg.weather_master_app.core.network.sources.airquality.accu + +import com.pranshulgg.weather_master_app.BuildConfig +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.AccuAqiForecastJson +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.AccuAqiJson +import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.json.AccuCurrentWeatherJson +import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.json.AccuDailyWeatherJson +import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.json.AccuHourlyWeatherJson +import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.json.AccuLocationJson +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query +import java.util.concurrent.TimeUnit + + +interface AccuAqiApi { + + @GET("airquality/v2/currentconditions/{locationKey}") + suspend fun fetchCurrent( + @Path("locationKey") locationKey: String, + @Query("pollutants") details: Boolean = true + ): Response + + @GET("airquality/v2/forecasts/hourly/24hour/{locationKey}") + suspend fun fetchForecast( + @Path("locationKey") locationKey: String, + @Query("pollutants") details: Boolean = true + ): Response + + + companion object { + const val BASE_URL = "https://api.accuweather.com/" + + fun create(): AccuAqiApi { + + + val auth = Interceptor { chain -> + val original = chain.request() + + val newUrl = + original.url.newBuilder().addQueryParameter("apikey", BuildConfig.ACCU_KEY) + .build() + val request = original.newBuilder().url(newUrl).build() + + chain.proceed(request) + } + + val client = OkHttpClient.Builder() + .addInterceptor(auth) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(AccuAqiApi::class.java) + } + } +} + diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/AccuAqiRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/AccuAqiRepository.kt new file mode 100644 index 000000000..0247ed19a --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/AccuAqiRepository.kt @@ -0,0 +1,95 @@ +package com.pranshulgg.weather_master_app.core.network.sources.airquality.accu + +import com.pranshulgg.weather_master_app.core.model.domain.AppException +import com.pranshulgg.weather_master_app.core.model.domain.location.Location +import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult +import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResult +import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResultType +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.bundle.AccuAqiJsonBundle +import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi +import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuApi +import com.pranshulgg.weather_master_app.core.utils.weather.cache.isCurrentAirQualitySafe +import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnAirQualityCache +import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao +import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao +import com.pranshulgg.weather_master_app.data.local.entity.airquality.accu.AccuEntity +import com.pranshulgg.weather_master_app.data.local.mapper.airquality.sources.accu.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.airquality.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.airquality.toEntity +import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.accu.toDomain +import com.pranshulgg.weather_master_app.data.repository.AirQualityRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.UnknownHostException +import javax.inject.Inject + + +class AccuAqiRepository @Inject constructor( + private val api: AccuAqiApi, + private val dao: AirQualityDao, + private val accuDao: AccuDao, + private val accuApi: AccuApi, +) : AirQualityRepository { + + override suspend fun getAirQuality( + location: Location, + isManualRefresh: Boolean, + isForceRefresh: Boolean + ): AirQualityResult = withContext(Dispatchers.IO) { + + + val cache = dao.getAirQualityForLocation(location.id) + val shouldReturnCache = shouldReturnAirQualityCache(cache, isManualRefresh, isForceRefresh) + + when (shouldReturnCache) { + AirQualityResultType.RETURN_CACHE -> return@withContext AirQualityResult.Success(cache!!.toDomain()) + else -> {} + } + + return@withContext try { + + val locationKey = accuDao.getCityKeyForLocation(location.id)?.toDomain()?.cityKey + ?: accuApi.getLocationKey("${location.latitude},${location.longitude}").body()?.key + ?: return@withContext AirQualityResult.Error(exception = AppException.Unknown()) + + + val responseCurrent = api.fetchCurrent(locationKey) + + val bodyCurrent = responseCurrent.body() + ?: return@withContext AirQualityResult.Error(exception = UnknownHostException()) + + val responseForecast = api.fetchForecast(locationKey) + + val bodyForecast = responseForecast.body() + ?: return@withContext AirQualityResult.Error(exception = UnknownHostException()) + + val final = AccuAqiJsonBundle( + current = bodyCurrent, + forecast = bodyForecast + ) + + val domain = final.toDomain() + + accuDao.insertCityKey( + AccuEntity( + locationId = location.id, + cityKey = locationKey + ) + ) + + dao.insertAirQuality( + domain.current.toEntity(location.id), + domain.hourly.map { it.toEntity(location.id) }, + location.id + ) + + AirQualityResult.Success(domain) + } catch (e: Exception) { + + val isCacheSafe = isCurrentAirQualitySafe(cache?.toDomain()) + + AirQualityResult.Error(exception = e, if (isCacheSafe) cache?.toDomain() else null) + } + } +} + diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/json/AccuAqiJson.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/json/AccuAqiJson.kt new file mode 100644 index 000000000..7362d5246 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/json/AccuAqiJson.kt @@ -0,0 +1,35 @@ +package com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json + +import com.google.gson.annotations.SerializedName + +data class AccuAqiJson( + val data: AccuAqiDataJson, + @SerializedName("epochDate") val timeSeconds: Long +) + +data class AccuAqiDataJson( + val pollutants: List +) + +data class AccuAqiPollutantJson( + val type: String, + val concentration: AccuAqiConcentrationJson +) + +data class AccuAqiConcentrationJson( + val value: Double? +) + +data class AccuAqiForecastJson( + val data: List +) + +data class AccuAqiForecastDataJson( + @SerializedName("epochDate") val timeSeconds: Long, + val pollutants: List +) + +data class AccuAqiForecastPollutantJson( + val type: String, + val concentration: AccuAqiConcentrationJson +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/json/bundle/AccuAqiJsonBundle.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/json/bundle/AccuAqiJsonBundle.kt new file mode 100644 index 000000000..84b2c11da --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/accu/json/bundle/AccuAqiJsonBundle.kt @@ -0,0 +1,9 @@ +package com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.bundle + +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.AccuAqiForecastJson +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.AccuAqiJson + +data class AccuAqiJsonBundle( + val current: AccuAqiJson, + val forecast: AccuAqiForecastJson +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/accu/AccuRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/accu/AccuRepository.kt index d6a004ef5..71302591c 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/accu/AccuRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/accu/AccuRepository.kt @@ -9,8 +9,10 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.bmkg.BmkgA import com.pranshulgg.weather_master_app.core.network.sources.weather.bmkg.json.bundle.BmkgForecastBundle import com.pranshulgg.weather_master_app.core.utils.weather.cache.isWeatherCacheSafe import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnWeatherCache +import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherDao +import com.pranshulgg.weather_master_app.data.local.entity.airquality.accu.AccuEntity import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.accu.toDomain import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.bmkg.toDomain import com.pranshulgg.weather_master_app.data.local.mapper.weather.toCurrentWeatherEntity @@ -27,7 +29,8 @@ import javax.inject.Inject class AccuRepository @Inject constructor( val dao: LocationsDao, val weatherDao: WeatherDao, - val api: AccuApi + val api: AccuApi, + val accuDao: AccuDao, ) : WeatherRepository { @@ -52,22 +55,22 @@ class AccuRepository @Inject constructor( return@withContext try { - val locationKey = api.getLocationKey("${location.latitude},${location.longitude}") - - val bodyLocation = locationKey.body() + val locationKey = accuDao.getCityKeyForLocation(location.id)?.cityKey + ?: api.getLocationKey("${location.latitude},${location.longitude}").body()?.key ?: return@withContext WeatherResult.Error(exception = AppException.Unknown()) - val current = api.fetchCurrent(bodyLocation.key) + + val current = api.fetchCurrent(locationKey) val bodyCurrent = current.body() ?: return@withContext WeatherResult.Error(exception = AppException.Unknown()) - val hourly = api.fetchHourly(bodyLocation.key) + val hourly = api.fetchHourly(locationKey) val bodyHourly = hourly.body() ?: return@withContext WeatherResult.Error(exception = AppException.Unknown()) - val daily = api.fetchDaily(bodyLocation.key) + val daily = api.fetchDaily(locationKey) val bodyDaily = daily.body() ?: return@withContext WeatherResult.Error(exception = AppException.Unknown()) @@ -80,6 +83,13 @@ class AccuRepository @Inject constructor( val domain = final.toDomain(location) + accuDao.insertCityKey( + AccuEntity( + locationId = location.id, + cityKey = locationKey + ) + ) + weatherDao.insertWeather( domain.current.toCurrentWeatherEntity(location.id), domain.hourly.toHourlyWeatherEntity(location.id), diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/cache/AirQualityCachePolicy.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/cache/AirQualityCachePolicy.kt index bed14909b..f51234552 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/cache/AirQualityCachePolicy.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/cache/AirQualityCachePolicy.kt @@ -1,6 +1,7 @@ package com.pranshulgg.weather_master_app.core.utils.weather.cache import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQuality +import com.pranshulgg.weather_master_app.core.model.weather.WeatherResultType import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResultType import com.pranshulgg.weather_master_app.core.utils.weather.cache.CacheConfig.AUTO_REFRESH_MAX_MINUTES import com.pranshulgg.weather_master_app.core.utils.weather.cache.CacheConfig.MANUAL_REFRESH_MINUTES @@ -20,9 +21,13 @@ fun isCurrentAirQualitySafe(airQuality: AirQuality?): Boolean { fun shouldReturnAirQualityCache( cache: AirQualityWithRelations?, - isManualRefresh: Boolean + isManualRefresh: Boolean, + isForceRefresh: Boolean ): AirQualityResultType { + if (isForceRefresh) return AirQualityResultType.ERROR + + if (cache == null || cache.current == null) { return AirQualityResultType.ERROR } diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/airquality/AirQualityDao.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/airquality/AirQualityDao.kt index b881c73c4..8c8935c96 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/airquality/AirQualityDao.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/airquality/AirQualityDao.kt @@ -18,9 +18,9 @@ interface AirQualityDao { hourlyAirQuality: List, id: String ) { - insertCurrentAirQuality(currentAirQuality) - + deleteCurrentAirQuality(id) deleteHourlyAirQuality(id) + insertCurrentAirQuality(currentAirQuality) insertHourlyAirQuality(hourlyAirQuality) } diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/airquality/accu/AccuDao.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/airquality/accu/AccuDao.kt new file mode 100644 index 000000000..e76db74bc --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/airquality/accu/AccuDao.kt @@ -0,0 +1,22 @@ +package com.pranshulgg.weather_master_app.data.local.dao.airquality.accu + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.pranshulgg.weather_master_app.data.local.entity.airquality.accu.AccuEntity +import com.pranshulgg.weather_master_app.data.local.entity.weather.nws.NwsGridPointsEntity + + +@Dao +interface AccuDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCityKey(entity: AccuEntity) + + @Query("SELECT * FROM accu_locations WHERE locationId = :locationId") + suspend fun getCityKeyForLocation(locationId: String): AccuEntity? + + @Query("DELETE FROM accu_locations WHERE locationId = :locationId") + suspend fun deleteCityKeyForLocation(locationId: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/airquality/accu/AccuEntity.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/airquality/accu/AccuEntity.kt new file mode 100644 index 000000000..8667d423d --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/airquality/accu/AccuEntity.kt @@ -0,0 +1,24 @@ +package com.pranshulgg.weather_master_app.data.local.entity.airquality.accu + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import com.pranshulgg.weather_master_app.data.local.entity.location.WeatherLocationEntity + +@Entity( + tableName = "accu_locations", + foreignKeys = [ + ForeignKey( + entity = WeatherLocationEntity::class, + parentColumns = ["id"], + childColumns = ["locationId"], + onDelete = ForeignKey.Companion.CASCADE + ) + ], + indices = [Index("locationId")], + primaryKeys = ["locationId"] +) +data class AccuEntity( + val locationId: String, + val cityKey: String +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/accu/AccuAqiMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/accu/AccuAqiMapper.kt new file mode 100644 index 000000000..89923509a --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/accu/AccuAqiMapper.kt @@ -0,0 +1,41 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.airquality.sources.accu + +import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQuality +import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQualityCurrent +import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQualityHourly +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.bundle.AccuAqiJsonBundle + + +fun AccuAqiJsonBundle.toDomain(): AirQuality { + + val current = this.current.data.pollutants.associate { it.type to it.concentration.value } + val hourly = this.forecast.data + + return AirQuality( + current = AirQualityCurrent( + usAqi = null, + pm10 = current["PM10"], + pm25 = current["PM2_5"], + carbonMonoxide = current["CO"], + nitrogenDioxide = current["NO2"], + sulphurDioxide = current["SO2"], + ozone = current["O3"], + lastUpdatedInMilli = System.currentTimeMillis() + ), + hourly = List(hourly.size) { index -> + + val pollutants = + hourly[index].pollutants.associate { it.type to it.concentration.value } + + AirQualityHourly( + time = hourly[index].timeSeconds, + pm10 = pollutants["PM10"], + pm25 = pollutants["PM2_5"], + carbonMonoxide = pollutants["CO"], + nitrogenDioxide = pollutants["NO2"], + sulphurDioxide = pollutants["SO2"], + ozone = pollutants["O3"] + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/accu/AccuEntityMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/accu/AccuEntityMapper.kt new file mode 100644 index 000000000..a23c3e1a0 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/accu/AccuEntityMapper.kt @@ -0,0 +1,23 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.accu + +import com.pranshulgg.weather_master_app.core.model.weather.airquality.accu.AccuCityKey +import com.pranshulgg.weather_master_app.data.local.entity.airquality.accu.AccuEntity + +// ---------------------------- ENTITY TO DOMAIN ---------------------------- + +fun AccuEntity.toDomain(): AccuCityKey { + return AccuCityKey( + locationId = locationId, + cityKey = cityKey + ) +} + + +// ---------------------------- DOMAIN TO ENTITY ---------------------------- + +fun AccuCityKey.toEntity(): AccuEntity { + return AccuEntity( + locationId = locationId, + cityKey = cityKey + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AirQualityRepositoryProvider.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AirQualityRepositoryProvider.kt new file mode 100644 index 000000000..608f6bc63 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AirQualityRepositoryProvider.kt @@ -0,0 +1,23 @@ +package com.pranshulgg.weather_master_app.data.provider + +import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiRepository +import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository +import com.pranshulgg.weather_master_app.data.repository.AirQualityRepository +import javax.inject.Inject + + +class AirQualityRepositoryProvider @Inject constructor( + private val openMeteoAqiRepository: OpenMeteoAqiRepository, + private val accuAqiRepository: AccuAqiRepository +) { + + fun getRepository(source: AirQualitySource): AirQualityRepository? { + return when (source) { + AirQualitySource.ACCU_WEATHER -> accuAqiRepository + AirQualitySource.OPEN_METEO -> openMeteoAqiRepository + AirQualitySource.NONE -> null + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AirQualityRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AirQualityRepository.kt new file mode 100644 index 000000000..c61fa868e --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AirQualityRepository.kt @@ -0,0 +1,15 @@ +package com.pranshulgg.weather_master_app.data.repository + +import com.pranshulgg.weather_master_app.core.model.domain.location.Location +import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult +import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResult + + +interface AirQualityRepository { + suspend fun getAirQuality( + location: Location, + isManualRefresh: Boolean = false, + isForceRefresh: Boolean + ): AirQualityResult + +} \ No newline at end of file From 80b32aae291d3c74a8b27b556f3d48bcf0561c0f Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:28:23 +0530 Subject: [PATCH 04/40] add AccuWeather AQI support and update repository dependencies --- .../weather_master_app/core/di/AppModule.kt | 10 +++++++--- .../weather_master_app/core/di/db/DaoModule.kt | 3 +++ .../core/di/network/ApiModule.kt | 5 +++++ .../core/di/weather/WeatherRepositoryModule.kt | 17 +++++++++++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/AppModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/AppModule.kt index 2694f46f5..01708f368 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/AppModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/AppModule.kt @@ -23,6 +23,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.openmeteo. import com.pranshulgg.weather_master_app.core.network.sources.weather.openmeteo.OpenMeteoRepository import com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao +import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao import com.pranshulgg.weather_master_app.data.local.dao.github.GithubDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherBlocksDao @@ -79,8 +80,11 @@ object AppModule { @Singleton fun provideWeatherDataReconcilerRepository( nwsDao: NwsDao, - locationsDao: LocationsDao - ): WeatherDataReconcilerRepository = WeatherDataReconcilerRepository(nwsDao, locationsDao) + locationsDao: LocationsDao, + accuDao: AccuDao, + airQualityDao: AirQualityDao + ): WeatherDataReconcilerRepository = + WeatherDataReconcilerRepository(nwsDao, locationsDao, accuDao, airQualityDao) @Provides @Singleton @@ -93,5 +97,5 @@ object AppModule { @Singleton fun provideNominatimRepository(api: NominatimApi): NominatimRepository = NominatimRepository(api) - + } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/db/DaoModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/db/DaoModule.kt index e055e4e84..e86622c71 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/db/DaoModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/db/DaoModule.kt @@ -34,4 +34,7 @@ object DaoModule { @Provides fun provideGithubDao(db: WeatherMasterDatabase) = db.githubDao() + + @Provides + fun provideAccuDao(db: WeatherMasterDatabase) = db.accuDao() } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt index 0012864e4..e953b97d8 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt @@ -2,6 +2,7 @@ package com.pranshulgg.weather_master_app.core.di.network import com.pranshulgg.weather_master_app.core.network.github.GithubApi import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim.NominatimApi +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiApi import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.GeoNamesSearchApi import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.timezone.GeoNamesTimezoneApi @@ -99,4 +100,8 @@ object ApiModule { @Provides @Singleton fun provideMeteoamApi(): MeteoamApi = MeteoamApi.create() + + @Provides + @Singleton + fun provideAccuAqiApi(): AccuAqiApi = AccuAqiApi.create() } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt index 0abb1ad24..d01562d13 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt @@ -1,5 +1,7 @@ package com.pranshulgg.weather_master_app.core.di.weather +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiApi +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiRepository import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuApi @@ -27,6 +29,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.openmeteo. import com.pranshulgg.weather_master_app.core.network.sources.weather.smhi.SmhiApi import com.pranshulgg.weather_master_app.core.network.sources.weather.smhi.SmhiRepository import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao +import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherDao import com.pranshulgg.weather_master_app.data.local.dao.weather.nws.NwsDao @@ -133,8 +136,9 @@ object WeatherRepositoryModule { fun provideAccuRepository( dao: LocationsDao, api: AccuApi, - weatherDao: WeatherDao - ): AccuRepository = AccuRepository(dao, weatherDao, api) + weatherDao: WeatherDao, + accuDao: AccuDao + ): AccuRepository = AccuRepository(dao, weatherDao, api, accuDao) @Provides @Singleton @@ -143,4 +147,13 @@ object WeatherRepositoryModule { api: MeteoamApi, weatherDao: WeatherDao ): MeteoamRepository = MeteoamRepository(dao, weatherDao, api) + + @Provides + @Singleton + fun provideAccuAqiRepository( + api: AccuAqiApi, + dao: AirQualityDao, + accuDao: AccuDao, + accuApi: AccuApi + ): AccuAqiRepository = AccuAqiRepository(api, dao, accuDao, accuApi) } \ No newline at end of file From 843c02d58c700e199988e4b1d2e7338328e328c5 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:28:53 +0530 Subject: [PATCH 05/40] implement AirQualityRepository in OpenMeteoAqiRepository and add domain mapper for OpenMeteo AQI --- .../openmeteo/OpenMeteoAqiRepository.kt | 11 ++++--- .../sources/openmeteo/OpenMeteoAqiMapper.kt | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/openmeteo/OpenMeteoAqiMapper.kt diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/openmeteo/OpenMeteoAqiRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/openmeteo/OpenMeteoAqiRepository.kt index 77847c87d..c4bed6a66 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/openmeteo/OpenMeteoAqiRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/airquality/openmeteo/OpenMeteoAqiRepository.kt @@ -6,8 +6,10 @@ import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualit import com.pranshulgg.weather_master_app.core.utils.weather.cache.isCurrentAirQualitySafe import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnAirQualityCache import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao +import com.pranshulgg.weather_master_app.data.local.mapper.airquality.sources.openmeteo.toDomain import com.pranshulgg.weather_master_app.data.local.mapper.airquality.toDomain import com.pranshulgg.weather_master_app.data.local.mapper.airquality.toEntity +import com.pranshulgg.weather_master_app.data.repository.AirQualityRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.net.UnknownHostException @@ -16,16 +18,17 @@ import javax.inject.Inject class OpenMeteoAqiRepository @Inject constructor( private val api: OpenMeteoAqiApi, private val dao: AirQualityDao -) { +) : AirQualityRepository { - suspend fun getAirQuality( + override suspend fun getAirQuality( location: Location, - isManualRefresh: Boolean + isManualRefresh: Boolean, + isForceRefresh: Boolean ): AirQualityResult = withContext(Dispatchers.IO) { val cache = dao.getAirQualityForLocation(location.id) - val shouldReturnCache = shouldReturnAirQualityCache(cache, isManualRefresh) + val shouldReturnCache = shouldReturnAirQualityCache(cache, isManualRefresh, isForceRefresh) when (shouldReturnCache) { AirQualityResultType.RETURN_CACHE -> return@withContext AirQualityResult.Success(cache!!.toDomain()) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/openmeteo/OpenMeteoAqiMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/openmeteo/OpenMeteoAqiMapper.kt new file mode 100644 index 000000000..f9da78116 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/openmeteo/OpenMeteoAqiMapper.kt @@ -0,0 +1,32 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.airquality.sources.openmeteo + +import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQuality +import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQualityCurrent +import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQualityHourly +import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiDto + + +fun OpenMeteoAqiDto.toDomain(): AirQuality = + AirQuality( + current = AirQualityCurrent( + usAqi = this.current.usAqi, + pm10 = this.current.pm10, + pm25 = this.current.pm25, + carbonMonoxide = this.current.carbonMonoxide, + nitrogenDioxide = this.current.nitrogenDioxide, + sulphurDioxide = this.current.sulphurDioxide, + ozone = this.current.ozone, + lastUpdatedInMilli = System.currentTimeMillis() + ), + hourly = List(hourly.time.size) { + AirQualityHourly( + time = hourly.time[it], + pm10 = hourly.pm10[it], + pm25 = hourly.pm25[it], + carbonMonoxide = hourly.carbonMonoxide[it], + nitrogenDioxide = hourly.carbonMonoxide[it], + sulphurDioxide = hourly.sulphurDioxide[it], + ozone = hourly.ozone[it] + ) + } + ) \ No newline at end of file From 235eac45693b562de2e1fdd17dcd49e5b4c0f556 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:29:03 +0530 Subject: [PATCH 06/40] rename OpenMeteoAqiMapper to AqiDomainMapper and remove OpenMeteoAqiDto mapping --- ...enMeteoAqiMapper.kt => AqiDomainMapper.kt} | 27 ------------------- 1 file changed, 27 deletions(-) rename app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/{OpenMeteoAqiMapper.kt => AqiDomainMapper.kt} (51%) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/OpenMeteoAqiMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/AqiDomainMapper.kt similarity index 51% rename from app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/OpenMeteoAqiMapper.kt rename to app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/AqiDomainMapper.kt index bb04f9207..acd6c7b43 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/OpenMeteoAqiMapper.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/AqiDomainMapper.kt @@ -1,37 +1,10 @@ package com.pranshulgg.weather_master_app.data.local.mapper.airquality -import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQuality import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQualityCurrent import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQualityHourly -import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiDto import com.pranshulgg.weather_master_app.data.local.entity.airquality.CurrentAirQualityEntity import com.pranshulgg.weather_master_app.data.local.entity.airquality.HourlyAirQualityEntity -fun OpenMeteoAqiDto.toDomain(): AirQuality = - AirQuality( - current = AirQualityCurrent( - usAqi = this.current.usAqi, - pm10 = this.current.pm10, - pm25 = this.current.pm25, - carbonMonoxide = this.current.carbonMonoxide, - nitrogenDioxide = this.current.nitrogenDioxide, - sulphurDioxide = this.current.sulphurDioxide, - ozone = this.current.ozone, - lastUpdatedInMilli = System.currentTimeMillis() - ), - hourly = List(hourly.time.size) { - AirQualityHourly( - time = hourly.time[it], - pm10 = hourly.pm10[it], - pm25 = hourly.pm25[it], - carbonMonoxide = hourly.carbonMonoxide[it], - nitrogenDioxide = hourly.carbonMonoxide[it], - sulphurDioxide = hourly.sulphurDioxide[it], - ozone = hourly.ozone[it] - ) - } - ) - fun AirQualityCurrent.toEntity(locationId: String): CurrentAirQualityEntity = CurrentAirQualityEntity( From efab4d940c304b7c2d6f284482131215845d9d5b Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:29:25 +0530 Subject: [PATCH 07/40] refine daily summary computation to use remaining hourly data and cleanup imports --- .../weather/computing/summary/ComputeDaySummary.kt | 11 +++++++++-- .../core/utils/weather/computing/summary/Headlines.kt | 4 +--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/computing/summary/ComputeDaySummary.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/computing/summary/ComputeDaySummary.kt index 7cd7628cc..2fea03981 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/computing/summary/ComputeDaySummary.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/computing/summary/ComputeDaySummary.kt @@ -13,6 +13,7 @@ import com.pranshulgg.weather_master_app.core.prefs.AppPrefsState import com.pranshulgg.weather_master_app.core.utils.formatters.safeZoneId import com.pranshulgg.weather_master_app.core.utils.locale.getCurrentAppLocale import com.pranshulgg.weather_master_app.core.utils.weather.computing.computeDailyWeatherCondition +import com.pranshulgg.weather_master_app.core.utils.weather.forecast.findHourlyIndexForTime import com.pranshulgg.weather_master_app.core.utils.weather.forecast.findMatchingHourly import java.time.Instant import java.time.format.DateTimeFormatter @@ -33,8 +34,13 @@ fun computeDaySummary( ) + val currentIndex = findHourlyIndexForTime( + hourly.map { it.time }, + weather.current.time + ) - val (day, night) = hourly.partition { forecast -> + + val (day, night) = hourly.drop(currentIndex).partition { forecast -> // consider 6am to 5pm daytime toHour(forecast.time, weather.location.timezone).toInt() in 6..17 @@ -158,4 +164,5 @@ private fun toHour(timeMilli: Long, zoneId: String): String { .withZone(safeZoneId(zoneId)) return formatter.format(instant) -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/computing/summary/Headlines.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/computing/summary/Headlines.kt index d479e7015..f05869ec9 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/computing/summary/Headlines.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/computing/summary/Headlines.kt @@ -1,12 +1,9 @@ package com.pranshulgg.weather_master_app.core.utils.weather.computing.summary import android.content.Context -import android.util.Log -import androidx.compose.ui.res.stringResource import com.pranshulgg.weather_master_app.R import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherUnits import com.pranshulgg.weather_master_app.core.model.weather.TemperatureUnit -import com.pranshulgg.weather_master_app.core.prefs.AppPrefsState import com.pranshulgg.weather_master_app.core.prefs.helper.PreferencesHelper import com.pranshulgg.weather_master_app.core.utils.formatters.to12HourTimeString import com.pranshulgg.weather_master_app.core.utils.formatters.to24HourTimeString @@ -134,6 +131,7 @@ fun getHeadline( val uvSentence = when { peakUv.uv >= 10 -> listOf( + context.getString(R.string.summary_uv_extreme_template_1, peakUvAt), context.getString(R.string.summary_uv_extreme_template_2, peakUvAt), context.getString(R.string.summary_uv_extreme_template_3, peakUvAt) From c2064256ff672f058a5a440db63ed58c6d4ff603 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:29:57 +0530 Subject: [PATCH 08/40] integrate AirQualityRepositoryProvider into WeatherViewModel and update stale data cleanup logic for air quality sources --- .../feature/shared/WeatherViewModel.kt | 84 +++++++++++-------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt index 16617ada6..59ef96fc2 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt @@ -1,6 +1,7 @@ package com.pranshulgg.weather_master_app.feature.shared import android.content.Context +import android.util.Log import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -18,6 +19,7 @@ import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResult import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager +import com.pranshulgg.weather_master_app.data.provider.AirQualityRepositoryProvider import com.pranshulgg.weather_master_app.data.provider.WeatherRepositoryProvider import com.pranshulgg.weather_master_app.data.repository.LocationsRepository import com.pranshulgg.weather_master_app.data.repository.WeatherBlocksRepository @@ -36,6 +38,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException @HiltViewModel class WeatherViewModel @Inject constructor( @@ -45,6 +48,7 @@ class WeatherViewModel @Inject constructor( private val weatherBlocksRepository: WeatherBlocksRepository, private val openMeteoAqiRepository: OpenMeteoAqiRepository, private val weatherDataReconcilerRepository: WeatherDataReconcilerRepository, + private val airQualityRepositoryProvider: AirQualityRepositoryProvider, @ApplicationContext private val context: Context ) : ViewModel() { @@ -125,7 +129,7 @@ class WeatherViewModel @Inject constructor( // Run separately if (!_uiState.value.isError) { launch { - handleAirQuality(location, isManualRefresh) + handleAirQuality(location, isManualRefresh, isForceRefreshForAirQuality) } } @@ -139,8 +143,10 @@ class WeatherViewModel @Inject constructor( delay(minLoadingTime - elapsed) } + setLoading(false) + } } @@ -166,33 +172,8 @@ class WeatherViewModel @Inject constructor( getWeather(location, location.source) } - fun updateSourceForLocation(location: Location, source: WeatherSource) { - val updatedLocation = location.copy(source = source) - viewModelScope.launch { - - locationsRepo.updateSourceForLocation(location.id, source) - val allowForceRefresh = location.source != source - - - if (allowForceRefresh) { - weatherDataReconcilerRepository.cleanUpStaleData(location.source, location.id) - } - _uiState.value = _uiState.value.copy( - activeLocation = updatedLocation - ) - getWeather( - updatedLocation, - source, - isForceRefresh = allowForceRefresh - ) - - - } - } - - - private fun handleSourceChangeForWeather( + fun handleSourceChangeForWeather( location: Location, source: WeatherSource, airQualitySource: AirQualitySource, @@ -209,7 +190,18 @@ class WeatherViewModel @Inject constructor( if (allowForceRefreshForWeather) { - weatherDataReconcilerRepository.cleanUpStaleData(location.source, location.id) + weatherDataReconcilerRepository.cleanUpStaleData( + location.source, + location.id, + airQualitySource + ) + } + if (allowForceRefreshForAirQuality) { + weatherDataReconcilerRepository.cleanUpStateAirQualityData( + airQualitySource, + location.id, + source + ) } _uiState.value = _uiState.value.copy( activeLocation = updatedLocation @@ -305,25 +297,47 @@ class WeatherViewModel @Inject constructor( } } - private suspend fun handleAirQuality(location: Location, isManualRefresh: Boolean) { + private suspend fun handleAirQuality( + location: Location, + isManualRefresh: Boolean, + isForceRefresh: Boolean + ) { if (_uiState.value.isAirQualityLoading) return + val repoAir = airQualityRepositoryProvider.getRepository(location.airQualitySource) + + + if (repoAir == null) { + _uiState.value = _uiState.value.copy(airQuality = null, isAirQualityLoading = false) + return + } + _uiState.value = _uiState.value.copy(isAirQualityLoading = true) - when (val result = openMeteoAqiRepository.getAirQuality(location, isManualRefresh)) { + + + when (val result = repoAir.getAirQuality(location, isManualRefresh, isForceRefresh)) { is AirQualityResult.Success -> { - _uiState.value = _uiState.value.copy(airQuality = result.airquality) - _uiState.value = _uiState.value.copy(isAirQualityLoading = false) + _uiState.value = + _uiState.value.copy( + airQuality = result.airquality, + isAirQualityLoading = false + ) + + } // Fail silently, we just won't show the Air quality in the UI is AirQualityResult.Error -> { - _uiState.value = _uiState.value.copy(airQuality = result.cacheAirQuality) - _uiState.value = _uiState.value.copy(isAirQualityLoading = false) + _uiState.value = _uiState.value.copy( + airQuality = result.cacheAirQuality, + isAirQualityLoading = false + ) + + } } } - } From c429f8ff3ec0150406479d2a47694237f6234e22 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:30:40 +0530 Subject: [PATCH 09/40] add updateLocationCustomName to LocationsDao and integrate AccuDao into WeatherMasterDatabase with version 49 bump --- .../data/local/WeatherMasterDatabase.kt | 12 +++++++++--- .../data/local/dao/location/LocationsDao.kt | 3 +++ .../data/repository/LocationsRepository.kt | 3 +++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt index 738c6dbea..c701e3a2a 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt @@ -8,6 +8,7 @@ import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao +import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao import com.pranshulgg.weather_master_app.data.local.dao.github.GithubDao import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherUnitsDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao @@ -17,6 +18,7 @@ import com.pranshulgg.weather_master_app.data.local.dao.weather.nws.NwsDao import com.pranshulgg.weather_master_app.data.local.entity.weather.units.AppWeatherUnitsEntity import com.pranshulgg.weather_master_app.data.local.entity.airquality.CurrentAirQualityEntity import com.pranshulgg.weather_master_app.data.local.entity.airquality.HourlyAirQualityEntity +import com.pranshulgg.weather_master_app.data.local.entity.airquality.accu.AccuEntity import com.pranshulgg.weather_master_app.data.local.entity.github.GithubEntity import com.pranshulgg.weather_master_app.data.local.entity.weather.CurrentWeatherEntity import com.pranshulgg.weather_master_app.data.local.entity.weather.DailyWeatherEntity @@ -36,15 +38,17 @@ import com.pranshulgg.weather_master_app.data.local.entity.weather.nws.NwsGridPo CurrentAirQualityEntity::class, NwsGridPointsEntity::class, GithubEntity::class, - HourlyAirQualityEntity::class + HourlyAirQualityEntity::class, + AccuEntity::class ], - version = 48, + version = 49, autoMigrations = [ AutoMigration(from = 39, to = 40), AutoMigration(from = 42, to = 43), AutoMigration(from = 43, to = 44), AutoMigration(from = 44, to = 45), - AutoMigration(from = 47, to = 48) + AutoMigration(from = 47, to = 48), + AutoMigration(from = 48, to = 49) ] ) abstract class WeatherMasterDatabase : RoomDatabase() { @@ -57,6 +61,8 @@ abstract class WeatherMasterDatabase : RoomDatabase() { abstract fun nwsDao(): NwsDao abstract fun githubDao(): GithubDao + abstract fun accuDao(): AccuDao + companion object { @Volatile diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt index 87cbae39a..d92548754 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt @@ -79,4 +79,7 @@ interface LocationsDao { @Query("UPDATE weather_locations SET airQualitySource = :source WHERE id = :id") suspend fun updateAirQualitySourceForLocation(id: String, source: AirQualitySource) + + @Query("UPDATE weather_locations SET customName = :name WHERE id = :id") + suspend fun updateLocationCustomName(id: String, name: String?) } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt index f28542cd0..33d7080e0 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt @@ -83,6 +83,9 @@ class LocationsRepository @Inject constructor( return dao.getLocationForId(id).toDomain() } + suspend fun updateLocationCustomName(id: String, name: String?) { + dao.updateLocationCustomName(id, name) + } @Transaction suspend fun saveLocation(location: Location) { From 687ec1195630e0f76cce4012113312b7e89fbbe8 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:30:53 +0530 Subject: [PATCH 10/40] add overline support to ActionTile and SettingTile.ActionTile --- .../weather_master_app/core/ui/components/SettingTile.kt | 4 +++- .../weather_master_app/core/ui/components/tiles/ActionTile.kt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/components/SettingTile.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/components/SettingTile.kt index 710d09990..985f198d6 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/components/SettingTile.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/components/SettingTile.kt @@ -82,6 +82,7 @@ sealed class SettingTile { val danger: Boolean = false, val selected: Boolean = false, val trailing: (@Composable (() -> Unit))? = null, + val overline: (@Composable (() -> Unit))? = null, ) : SettingTile() @@ -216,7 +217,8 @@ fun SettingSection( danger = tile.danger, itemBgColor = itemBgColor, selected = tile.selected, - trailing = tile.trailing + trailing = tile.trailing, + overline = tile.overline ) is SettingTile.SwitchTile -> SwitchTile( diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/components/tiles/ActionTile.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/components/tiles/ActionTile.kt index 6f9ed1fb4..43184cee1 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/components/tiles/ActionTile.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/components/tiles/ActionTile.kt @@ -24,7 +24,8 @@ fun ActionTile( danger: Boolean = false, itemBgColor: Color, selected: Boolean = false, - trailing: @Composable (() -> Unit)? = null + trailing: @Composable (() -> Unit)? = null, + overline: @Composable (() -> Unit)? = null, ) { Surface( @@ -55,6 +56,7 @@ fun ActionTile( ) } }, + overlineContent = overline, trailingContent = trailing ) } From 06b7c6648db7bd7a52598d961dc55b4f7d618f90 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:31:04 +0530 Subject: [PATCH 11/40] implement saving custom location names and add GetLocationName utility functions --- .../utils/weather/location/GetLocationName.kt | 46 +++++++++++++++++++ .../editlocation/EditLocationScreen.kt | 27 +++++++---- .../editlocation/EditLocationViewModel.kt | 6 +++ 3 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/location/GetLocationName.kt diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/location/GetLocationName.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/location/GetLocationName.kt new file mode 100644 index 000000000..10931ac51 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/location/GetLocationName.kt @@ -0,0 +1,46 @@ +package com.pranshulgg.weather_master_app.core.utils.weather.location + +import com.pranshulgg.weather_master_app.core.model.domain.location.Location + +fun getLocationNameInShort(location: Location?): String { + + if (location == null) { + return "••••" + } + val isCustomNameValid = !location.customName.isNullOrBlank() + + return if (isCustomNameValid) { + location.customName + } else { + location.name + } +} + +fun getFullLocationName(location: Location?): String { + + if (location == null) { + return "••••" + } + + val isCustomNameValid = !location.customName.isNullOrBlank() + + val locationText = location.let { + buildString { + append(location.name) + if (location.country.isNotBlank()) { + append(", ") + } + if (location.state.isNotBlank()) { + append(location.state) + append(", ") + } + append(location.country) + } + } + + return if (isCustomNameValid) { + location.customName + } else { + locationText + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt index ec0708989..f25703013 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt @@ -1,8 +1,6 @@ package com.pranshulgg.weather_master_app.feature.editlocation -import android.util.Log import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -38,8 +36,6 @@ import com.pranshulgg.weather_master_app.core.model.domain.location.Location import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource import com.pranshulgg.weather_master_app.core.model.sources.AlertSource import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource -import com.pranshulgg.weather_master_app.core.model.sources.getWeatherSourcesForCountry -import com.pranshulgg.weather_master_app.core.model.sources.getWeatherSourcesGlobal import com.pranshulgg.weather_master_app.core.ui.components.Gap import com.pranshulgg.weather_master_app.core.ui.components.LargeTopBarScaffold import com.pranshulgg.weather_master_app.core.ui.components.NavigateUpBtn @@ -47,6 +43,7 @@ import com.pranshulgg.weather_master_app.core.ui.components.SettingSection import com.pranshulgg.weather_master_app.core.ui.components.SettingTile import com.pranshulgg.weather_master_app.core.ui.components.Symbol import com.pranshulgg.weather_master_app.feature.editlocation.ui.EditLocationBottomSheet +import com.pranshulgg.weather_master_app.feature.shared.WeatherViewModel import com.pranshulgg.weather_master_app.feature.shared.ui.SharedBottomSheet @@ -64,7 +61,8 @@ data class EditLocationScreenUiState( @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable fun EditLocationScreen( - navController: NavController, id: String + navController: NavController, id: String, + weatherViewModel: WeatherViewModel ) { val viewModel: EditLocationViewModel = hiltViewModel() @@ -125,11 +123,12 @@ fun EditLocationScreen( viewModel.showEditLocationNameSheet() }, trailing = { - IconButton(onClick = { currentLocationName = locationName }) { + IconButton(onClick = { currentLocationName = locationText }) { Symbol(R.drawable.refresh_24px) } }, - colorDesc = colorDesc + colorDesc = colorDesc, + overline = { Text("Requires restart") } ) ) ) @@ -179,7 +178,19 @@ fun EditLocationScreen( .heightIn(btnSize) .fillMaxWidth() .padding(horizontal = 16.dp), - onClick = { /* Handle click */ }, + onClick = { + viewModel.saveLocationName( + if (currentLocationName.trim() == locationText.trim()) null else currentLocationName.trim(), + uiState.location.id + ) + + navController.popBackStack() + weatherViewModel.handleSourceChangeForWeather( + uiState.location, + uiState.selectedWeatherSource ?: uiState.location.source, + uiState.selectedAirQualitySource ?: uiState.location.airQualitySource + ) + }, shapes = ButtonDefaults.shapes(), contentPadding = ButtonDefaults.contentPaddingFor(btnSize), ) { diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationViewModel.kt index 1d743d48b..0922f9307 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationViewModel.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationViewModel.kt @@ -31,6 +31,12 @@ class EditLocationViewModel @Inject constructor( } + fun saveLocationName(name: String?, id: String) { + viewModelScope.launch { + locationsRepo.updateLocationCustomName(id, name) + } + } + fun showWeatherSourcesForLocationSheet() { _uiState.value = _uiState.value.copy(isWeatherSourcesForLocationSheetOpen = true) } From c2b424f347c5441bd2b424445fbffb633953f05a Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:31:15 +0530 Subject: [PATCH 12/40] implement air quality and AccuWeather data cleanup in WeatherDataReconcilerRepository --- .../WeatherDataReconcilerRepository.kt | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt index d85a6b563..77e5719ff 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt @@ -1,6 +1,9 @@ package com.pranshulgg.weather_master_app.data.repository +import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource +import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao +import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao import com.pranshulgg.weather_master_app.data.local.dao.weather.nws.NwsDao import jakarta.inject.Inject @@ -8,7 +11,9 @@ import jakarta.inject.Inject class WeatherDataReconcilerRepository @Inject constructor( private val nwsDao: NwsDao, - private val locationDao: LocationsDao + private val locationDao: LocationsDao, + private val accuDao: AccuDao, + private val airQualityDao: AirQualityDao ) { /** @@ -17,14 +22,37 @@ class WeatherDataReconcilerRepository @Inject constructor( * NWS might have saved grid points * which are important to be removed from the DB so they don't end up stale */ - suspend fun cleanUpStaleData(previousSource: WeatherSource, locationId: String) { + suspend fun cleanUpStaleData( + previousSource: WeatherSource, + locationId: String, + airQualitySource: AirQualitySource + ) { when (previousSource) { - WeatherSource.NWS -> cleanNws(locationId) + WeatherSource.NWS -> nwsDao.deleteGridPointsForLocation(locationId) + WeatherSource.ACCU_WEATHER -> cleanAccuWeather(locationId, airQualitySource) else -> {} } } - suspend fun cleanNws(locationId: String) { - nwsDao.deleteGridPointsForLocation(locationId) + suspend fun cleanAccuWeather(locationId: String, airQualitySource: AirQualitySource) { + if (airQualitySource != AirQualitySource.ACCU_WEATHER) { + accuDao.deleteCityKeyForLocation(locationId) + } } + + suspend fun cleanUpStateAirQualityData( + currentSource: AirQualitySource, + locationId: String, + currentWeatherSource: WeatherSource + ) { + if (currentSource == AirQualitySource.NONE) { + airQualityDao.deleteCurrentAirQuality(locationId) + airQualityDao.deleteHourlyAirQuality(locationId) + } + + if (currentWeatherSource != WeatherSource.ACCU_WEATHER) { + accuDao.deleteCityKeyForLocation(locationId) + } + } + } \ No newline at end of file From bccf0092b3c90d35dc4f00f97c78dac894ac8cd4 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:31:25 +0530 Subject: [PATCH 13/40] use getFullLocationName utility in MainSearchBar --- .../feature/main/components/MainSearchBar.kt | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/components/MainSearchBar.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/components/MainSearchBar.kt index 1b8cacb1f..5a9ee773a 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/components/MainSearchBar.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/components/MainSearchBar.kt @@ -30,6 +30,7 @@ import com.pranshulgg.weather_master_app.core.ui.components.Tooltip import com.pranshulgg.weather_master_app.core.ui.navigation.NavRoutes import com.pranshulgg.weather_master_app.core.ui.theme.ShadowElevation import com.pranshulgg.weather_master_app.core.ui.theme.isThemeDark +import com.pranshulgg.weather_master_app.core.utils.weather.location.getFullLocationName import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -52,20 +53,6 @@ fun MainSearchBar( } } - val locationText = activeLocation?.let { - buildString { - append(activeLocation.name) - if (activeLocation.country.isNotBlank()) { - append(", ") - } - if (activeLocation.state.isNotBlank()) { - append(activeLocation.state) - append(", ") - } - append(activeLocation.country) - } - } ?: "••••" - Surface( color = if (isFroggyLayout) getSearchBarColor(isDark = isThemeDark()) else Color.Transparent, @@ -107,7 +94,7 @@ fun MainSearchBar( } Gap(horizontal = 4.dp) Text( - locationText, + getFullLocationName(activeLocation), color = if (isFroggyLayout) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f), From bf0ceca34d106b1ae25968a610d128137cafe3be Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:31:36 +0530 Subject: [PATCH 14/40] navigate to edit location screen in MainScreen and update air quality source display --- .../feature/main/MainScreen.kt | 33 ++++++++++--------- .../feature/main/ui/MainScreenBottomSheets.kt | 7 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreen.kt index fd216af97..0f369b664 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreen.kt @@ -29,6 +29,7 @@ import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherBlock import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherUnits import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource +import com.pranshulgg.weather_master_app.core.ui.navigation.NavRoutes import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager import com.pranshulgg.weather_master_app.feature.intro.IntroScreen import com.pranshulgg.weather_master_app.feature.locations.LocationsScreen @@ -63,8 +64,7 @@ data class MainScreenUiState( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MainScreen(navController: NavController) { - val weatherViewModel: WeatherViewModel = hiltViewModel() +fun MainScreen(navController: NavController, weatherViewModel: WeatherViewModel) { val uiState by weatherViewModel.uiState val viewModel: MainScreenViewModel = hiltViewModel() val mainScreenUiState = viewModel.uiState.value @@ -166,7 +166,8 @@ fun MainScreen(navController: NavController) { } }, onEditLocation = { - viewModel.showWeatherSourcesForLocationSheet(uiState.isLoading) +// viewModel.showWeatherSourcesForLocationSheet(uiState.isLoading) + navController.navigate(NavRoutes.editLocation(activeLocation!!.id)) }, context, onWeatherSourceInfoClick = viewModel::showWeatherSourcesInfoForLocationSheet, @@ -177,19 +178,19 @@ fun MainScreen(navController: NavController) { // WEATHER SOURCES DIALOG - SharedBottomSheet.WeatherSourcesForLocationSheet( - countryCode = activeLocation?.countryCode, - show = viewModel.uiState.value.isWeatherSourcesForLocationSheetOpen, - isEditing = true, - selectedSource = activeLocation?.source ?: WeatherSource.OPEN_METEO, - onSave = { - if (activeLocation != null) { - weatherViewModel.updateSourceForLocation(activeLocation, it) - } - }, - onDismiss = viewModel::hideWeatherSourcesForLocationSheet, - sheetState = sheetState - ) +// SharedBottomSheet.WeatherSourcesForLocationSheet( +// countryCode = activeLocation?.countryCode, +// show = viewModel.uiState.value.isWeatherSourcesForLocationSheetOpen, +// isEditing = true, +// selectedSource = activeLocation?.source ?: WeatherSource.OPEN_METEO, +// onSave = { +// if (activeLocation != null) { +// weatherViewModel.updateSourceForLocation(activeLocation, it) +// } +// }, +// onDismiss = viewModel::hideWeatherSourcesForLocationSheet, +// sheetState = sheetState +// ) // WEATHER SOURCES INFO DIALOG MainScreenBottomSheets.WeatherSourcesInfoForLocationSheet(viewModel, activeLocation, sheetState) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/MainScreenBottomSheets.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/MainScreenBottomSheets.kt index 2e336f9e0..9bc4d8894 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/MainScreenBottomSheets.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/MainScreenBottomSheets.kt @@ -42,7 +42,6 @@ object MainScreenBottomSheets { val uiState = viewModel.uiState.value val uriHandler = LocalUriHandler.current - val airQualityUrl = "https://open-meteo.com/en/docs/air-quality-api" if (uiState.isWeatherSourcesInfoForLocationSheetOpen) { ActionBottomSheet( @@ -70,11 +69,11 @@ object MainScreenBottomSheets { title = stringResource(R.string.weather_air_quality), tiles = listOf( SettingTile.ActionTile( - title = WeatherSource.OPEN_METEO.displayName, - description = airQualityUrl, + title = location.airQualitySource.fullName, + description = location.airQualitySource.displayLink, trailing = { SettingsTileIcon(R.drawable.open_in_new_24px) }, onClick = { - uriHandler.openUri(airQualityUrl) + uriHandler.openUri(location.airQualitySource.displayLink) } ) ) From ea10e14735d07e3ac32a87f325ddca222213832b Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:31:45 +0530 Subject: [PATCH 15/40] add info button to LocationsScreen top bar to show long press info snackbar --- .../feature/locations/LocationsScreen.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt index c8cce5509..40dab2782 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt @@ -110,7 +110,7 @@ fun LocationsScreen( floatingActionButton = { FloatingButton(navController) }, - floatingActionButtonPosition = FabPosition.Center + floatingActionButtonPosition = FabPosition.Center, ) { paddingValues -> Box( modifier = Modifier @@ -185,6 +185,13 @@ private fun TopBar(onBack: () -> Unit, isTabletLike: Boolean) { } } } + }, + actions = { + IconButton(onClick = { + SnackbarManager.show(R.string.location_long_press_info) + }, shapes = IconButtonDefaults.shapes()) { + Symbol(R.drawable.info_24px) + } } ) } From ef58d661fc459015ceeb7c53c929b4a92d1b6963 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Mon, 27 Jul 2026 16:31:58 +0530 Subject: [PATCH 16/40] Update database schema to version 49 --- .../49.json | 916 ++++++++++++++++++ 1 file changed, 916 insertions(+) create mode 100644 app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/49.json diff --git a/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/49.json b/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/49.json new file mode 100644 index 000000000..22b768b5b --- /dev/null +++ b/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/49.json @@ -0,0 +1,916 @@ +{ + "formatVersion": 1, + "database": { + "version": 49, + "identityHash": "41f3af0cd4a758a20290098071993b9c", + "entities": [ + { + "tableName": "weather_locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `country` TEXT NOT NULL, `lat` REAL NOT NULL, `lon` REAL NOT NULL, `timezone` TEXT NOT NULL, `source` TEXT NOT NULL, `state` TEXT, `isFavorite` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL, `countryCode` TEXT, `isDefault` INTEGER NOT NULL, `isDeviceLocation` INTEGER NOT NULL, `alertSource` TEXT NOT NULL, `airQualitySource` TEXT NOT NULL, `customName` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "country", + "columnName": "country", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lat", + "columnName": "lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lon", + "columnName": "lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT" + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPinned", + "columnName": "isPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countryCode", + "columnName": "countryCode", + "affinity": "TEXT" + }, + { + "fieldPath": "isDefault", + "columnName": "isDefault", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeviceLocation", + "columnName": "isDeviceLocation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alertSource", + "columnName": "alertSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "airQualitySource", + "columnName": "airQualitySource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "customName", + "columnName": "customName", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "weather_current", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperature` REAL, `humidity` REAL NOT NULL, `windSpeed` REAL, `windDirection` TEXT, `pressureMsl` REAL, `visibility` INTEGER, `cloudCover` REAL, `uvIndex` REAL, `weatherCondition` TEXT NOT NULL, `feelsLike` REAL, `time` INTEGER NOT NULL, `dewPoint` REAL, `utcOffsetSeconds` INTEGER, `lastUpdatedInMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudCover", + "columnName": "cloudCover", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndex", + "columnName": "uvIndex", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feelsLike", + "columnName": "feelsLike", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "utcOffsetSeconds", + "columnName": "utcOffsetSeconds", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdatedInMilli", + "columnName": "lastUpdatedInMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_weather_current_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_current_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_hourly", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperature` REAL, `windSpeed` REAL, `windDirection` TEXT, `rain` REAL NOT NULL, `snowfall` REAL, `uvIndex` REAL, `weatherCondition` TEXT NOT NULL, `pressureMsl` REAL, `visibility` INTEGER, `humidity` REAL, `dewPoint` REAL, `time` INTEGER NOT NULL, `precipitationProbability` INTEGER, PRIMARY KEY(`locationId`, `time`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL" + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snowfall", + "columnName": "snowfall", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndex", + "columnName": "uvIndex", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL" + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "precipitationProbability", + "columnName": "precipitationProbability", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId", + "time" + ] + }, + "indices": [ + { + "name": "index_weather_hourly_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_hourly_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_daily", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperatureMin` REAL, `temperatureMax` REAL, `windSpeed` REAL, `windDirection` TEXT, `rainSum` REAL NOT NULL, `snowfallSum` REAL, `uvIndexMax` REAL, `weatherCondition` TEXT NOT NULL, `time` INTEGER NOT NULL, `precipitationProbabilityMax` INTEGER, `pressureMsl` REAL, `visibility` INTEGER, `humidity` REAL, `dewPoint` REAL, `sunrise` INTEGER NOT NULL, `sunset` INTEGER NOT NULL, `moonrise` INTEGER NOT NULL, `moonset` INTEGER NOT NULL, `moonPhase` TEXT NOT NULL, `dawn` INTEGER NOT NULL, `dusk` INTEGER NOT NULL, PRIMARY KEY(`locationId`, `time`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperatureMin", + "columnName": "temperatureMin", + "affinity": "REAL" + }, + { + "fieldPath": "temperatureMax", + "columnName": "temperatureMax", + "affinity": "REAL" + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "rainSum", + "columnName": "rainSum", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snowfallSum", + "columnName": "snowfallSum", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndexMax", + "columnName": "uvIndexMax", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "precipitationProbabilityMax", + "columnName": "precipitationProbabilityMax", + "affinity": "INTEGER" + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL" + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "sunrise", + "columnName": "sunrise", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sunset", + "columnName": "sunset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonrise", + "columnName": "moonrise", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonset", + "columnName": "moonset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonPhase", + "columnName": "moonPhase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dawn", + "columnName": "dawn", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dusk", + "columnName": "dusk", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId", + "time" + ] + }, + "indices": [ + { + "name": "index_weather_daily_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_daily_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_units", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tempUnit` TEXT NOT NULL, `windUnit` TEXT NOT NULL, `distanceUnit` TEXT NOT NULL, `pressureUnit` TEXT NOT NULL, `precipitationUnit` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tempUnit", + "columnName": "tempUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "windUnit", + "columnName": "windUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "distanceUnit", + "columnName": "distanceUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pressureUnit", + "columnName": "pressureUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precipitationUnit", + "columnName": "precipitationUnit", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "weather_blocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isDaily` INTEGER NOT NULL, `type` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDaily", + "columnName": "isDaily", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "air_quality_current", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `usAqi` INTEGER, `pm10` REAL, `pm25` REAL, `carbonMonoxide` REAL, `nitrogenDioxide` REAL, `sulphurDioxide` REAL, `ozone` REAL, `lastUpdatedInMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usAqi", + "columnName": "usAqi", + "affinity": "INTEGER" + }, + { + "fieldPath": "pm10", + "columnName": "pm10", + "affinity": "REAL" + }, + { + "fieldPath": "pm25", + "columnName": "pm25", + "affinity": "REAL" + }, + { + "fieldPath": "carbonMonoxide", + "columnName": "carbonMonoxide", + "affinity": "REAL" + }, + { + "fieldPath": "nitrogenDioxide", + "columnName": "nitrogenDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "sulphurDioxide", + "columnName": "sulphurDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "ozone", + "columnName": "ozone", + "affinity": "REAL" + }, + { + "fieldPath": "lastUpdatedInMilli", + "columnName": "lastUpdatedInMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_air_quality_current_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_air_quality_current_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nws_location_gridpoints", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `officeId` TEXT NOT NULL, `gridX` INTEGER NOT NULL, `gridY` INTEGER NOT NULL, `stationIdentifier` TEXT NOT NULL, `lastUpdatedMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "officeId", + "columnName": "officeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gridX", + "columnName": "gridX", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gridY", + "columnName": "gridY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stationIdentifier", + "columnName": "stationIdentifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdatedMilli", + "columnName": "lastUpdatedMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_nws_location_gridpoints_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nws_location_gridpoints_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "github", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `currentTag` TEXT NOT NULL, `lastFetchedTag` TEXT NOT NULL, `lastFetchedTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentTag", + "columnName": "currentTag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchedTag", + "columnName": "lastFetchedTag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchedTime", + "columnName": "lastFetchedTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "air_quality_hourly", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `locationId` TEXT NOT NULL, `pm10` REAL, `pm25` REAL, `carbonMonoxide` REAL, `nitrogenDioxide` REAL, `sulphurDioxide` REAL, `ozone` REAL, `time` INTEGER NOT NULL, FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pm10", + "columnName": "pm10", + "affinity": "REAL" + }, + { + "fieldPath": "pm25", + "columnName": "pm25", + "affinity": "REAL" + }, + { + "fieldPath": "carbonMonoxide", + "columnName": "carbonMonoxide", + "affinity": "REAL" + }, + { + "fieldPath": "nitrogenDioxide", + "columnName": "nitrogenDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "sulphurDioxide", + "columnName": "sulphurDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "ozone", + "columnName": "ozone", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_air_quality_hourly_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_air_quality_hourly_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "accu_locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `cityKey` TEXT NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cityKey", + "columnName": "cityKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_accu_locations_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_accu_locations_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '41f3af0cd4a758a20290098071993b9c')" + ] + } +} \ No newline at end of file From c2757ea4b567d62db3844ffd9162736a042091a2 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 09:40:32 +0530 Subject: [PATCH 17/40] add alert severity strings to strings.xml --- app/src/main/res/values/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eab37fa6d..e441cc845 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -383,6 +383,13 @@ Location information Long press a location for more options + Critical + High + Medium + Low + Unknown + + From f36498c5c23c39d1c941d13f59516a7ab2d07009 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:30:47 +0530 Subject: [PATCH 18/40] weather alerts --- .../core/model/domain/alerts/Alert.kt | 15 ++++ .../core/model/weather/alerts/AlertResult.kt | 15 ++++ .../model/weather/alerts/AlertSeverity.kt | 30 ++++++++ .../sources/alerts/accu/AlertsAccuApi.kt | 59 +++++++++++++++ .../alerts/accu/AlertsAccuRepository.kt | 72 +++++++++++++++++++ .../alerts/accu/json/AlertsAccuJson.kt | 24 +++++++ .../utils/weather/cache/AlertCachePolicy.kt | 38 ++++++++++ .../data/local/dao/alerts/AlertsDao.kt | 31 ++++++++ .../data/local/entity/alerts/AlertEntity.kt | 34 +++++++++ .../local/mapper/alerts/AlertDomainMapper.kt | 31 ++++++++ .../alerts/sources/accu/AccuAlertsMapper.kt | 29 ++++++++ .../data/provider/AlertsRepositoryProvider.kt | 21 ++++++ .../data/repository/AlertRepository.kt | 12 ++++ .../feature/main/ui/AlertsSection.kt | 71 ++++++++++++++++++ 14 files changed, 482 insertions(+) create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/model/domain/alerts/Alert.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertResult.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/AlertsAccuApi.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/AlertsAccuRepository.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/json/AlertsAccuJson.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/cache/AlertCachePolicy.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/alerts/AlertEntity.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/AlertDomainMapper.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/accu/AccuAlertsMapper.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AlertRepository.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/domain/alerts/Alert.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/domain/alerts/Alert.kt new file mode 100644 index 000000000..433394a4d --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/domain/alerts/Alert.kt @@ -0,0 +1,15 @@ +package com.pranshulgg.weather_master_app.core.model.domain.alerts + +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity + + +data class Alert( + val locationId: String, + val event: String, + val severity: AlertSeverity?, + val effective: Long, + val expires: Long, + val description: String, + val source: String?, + val lastUpdatedInMilli: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertResult.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertResult.kt new file mode 100644 index 000000000..e4958f00d --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertResult.kt @@ -0,0 +1,15 @@ +package com.pranshulgg.weather_master_app.core.model.weather.alerts + +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert + + +sealed class AlertResult { + data class Success(val alerts: List) : AlertResult() + data class Error(val exception: Exception, val cacheAlerts: List = emptyList()) : + AlertResult() +} + +enum class AlertResultType { + RETURN_CACHE, + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt new file mode 100644 index 000000000..7203f0f83 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt @@ -0,0 +1,30 @@ +package com.pranshulgg.weather_master_app.core.model.weather.alerts + +import androidx.compose.ui.graphics.Color +import com.pranshulgg.weather_master_app.R + +enum class AlertSeverity( + val label: Int, + val color: Color +) { + CRITICAL( + R.string.alert_severity_critical, + Color(0xFFD32F2F) + ), + HIGH( + R.string.alert_severity_high, + Color(0xFFF57C00) + ), + MODERATE( + R.string.alert_severity_medium, + Color(0xFFFBC02D) + ), + LOW( + R.string.alert_severity_low, + Color(0xFF388E3C) + ), + UNKNOWN( + R.string.alert_severity_unknown, + Color(0xFF757575) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/AlertsAccuApi.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/AlertsAccuApi.kt new file mode 100644 index 000000000..cbe21253e --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/AlertsAccuApi.kt @@ -0,0 +1,59 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.accu + +import com.pranshulgg.weather_master_app.BuildConfig +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.AccuAqiForecastJson +import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.AccuAqiJson +import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.json.AlertsAccuJson +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query +import java.util.concurrent.TimeUnit + + +interface AlertsAccuApi { + + @GET("alerts/v1/{locationKey}") + suspend fun fetchAlerts( + @Path("locationKey") locationKey: String, + @Query("details") details: Boolean = true + ): Response> + + + companion object { + const val BASE_URL = "https://api.accuweather.com/" + + fun create(): AlertsAccuApi { + + + val auth = Interceptor { chain -> + val original = chain.request() + + val newUrl = + original.url.newBuilder().addQueryParameter("apikey", BuildConfig.ACCU_KEY) + .build() + val request = original.newBuilder().url(newUrl).build() + + chain.proceed(request) + } + + val client = OkHttpClient.Builder() + .addInterceptor(auth) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(AlertsAccuApi::class.java) + } + } +} + diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/AlertsAccuRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/AlertsAccuRepository.kt new file mode 100644 index 000000000..530a7f642 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/AlertsAccuRepository.kt @@ -0,0 +1,72 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.accu + +import com.pranshulgg.weather_master_app.core.model.domain.AppException +import com.pranshulgg.weather_master_app.core.model.domain.location.Location +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResultType +import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuApi +import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnAlertsCache +import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao +import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao +import com.pranshulgg.weather_master_app.data.local.entity.airquality.accu.AccuEntity +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.sources.accu.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toEntity +import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.accu.toDomain +import com.pranshulgg.weather_master_app.data.repository.AlertRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + + +class AlertsAccuRepository @Inject constructor( + private val api: AlertsAccuApi, + private val dao: AlertsDao, + private val accuDao: AccuDao, + private val accuApi: AccuApi, +) : AlertRepository { + override suspend fun getAlerts( + location: Location, + isManualRefresh: Boolean, + isForceRefresh: Boolean + ): AlertResult = withContext(Dispatchers.IO) { + + val cache = dao.getAlertsForLocation(location.id) + val shouldReturnCache = shouldReturnAlertsCache(cache, isManualRefresh, isForceRefresh) + + when (shouldReturnCache) { + AlertResultType.RETURN_CACHE -> return@withContext AlertResult.Success(cache.map { it!!.toDomain() }) + else -> {} + } + + return@withContext try { + val locationKey = accuDao.getCityKeyForLocation(location.id)?.toDomain()?.cityKey + ?: accuApi.getLocationKey("${location.latitude},${location.longitude}").body()?.key + ?: return@withContext AlertResult.Error(exception = AppException.Unknown()) + + val response = api.fetchAlerts(locationKey) + val body = response.body() + ?: return@withContext AlertResult.Error(exception = AppException.Unknown()) + + val domain = body.map { it.toDomain(location.id) } + + accuDao.insertCityKey( + AccuEntity( + locationId = location.id, + cityKey = locationKey + ) + ) + dao.insertAlerts( + domain.map { it.toEntity(location.id) }, + location.id + ) + + AlertResult.Success(domain) + + } catch (e: Exception) { + AlertResult.Error(exception = e, cacheAlerts = cache.map { it!!.toDomain() }) + } + + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/json/AlertsAccuJson.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/json/AlertsAccuJson.kt new file mode 100644 index 000000000..2611e840c --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/accu/json/AlertsAccuJson.kt @@ -0,0 +1,24 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.json + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + + +data class AlertsAccuJson( + @SerializedName("Description") val event: AlertsAccuEventJson, + @SerializedName("AlarmLevel") val alarmLevel: String?, + @SerializedName("Source") val source: String?, + @SerializedName("Area") val area: List, +) + +data class AlertsAccuEventJson( + @SerializedName("English") val english: String, + @SerializedName("Localized") val localized: String +) + +data class AlertsAccuAreaJson( + @SerializedName("EpochStartTime") val epochStartTimeSeconds: Long, + @SerializedName("EpochEndTime") val epochEndTimeSeconds: Long, + @SerializedName("Text") val text: String, + + ) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/cache/AlertCachePolicy.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/cache/AlertCachePolicy.kt new file mode 100644 index 000000000..e07582ce5 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/cache/AlertCachePolicy.kt @@ -0,0 +1,38 @@ +package com.pranshulgg.weather_master_app.core.utils.weather.cache + +import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResultType +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResultType +import com.pranshulgg.weather_master_app.core.utils.weather.cache.CacheConfig.AUTO_REFRESH_MAX_MINUTES +import com.pranshulgg.weather_master_app.core.utils.weather.cache.CacheConfig.MANUAL_REFRESH_MINUTES +import com.pranshulgg.weather_master_app.data.local.entity.airquality.AirQualityWithRelations +import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity +import java.util.concurrent.TimeUnit + + +fun shouldReturnAlertsCache( + cache: List, + isManualRefresh: Boolean, + isForceRefresh: Boolean +): AlertResultType { + + if (isForceRefresh) return AlertResultType.ERROR + + + if (cache.isEmpty() || cache[0] == null) { + return AlertResultType.ERROR + } + + + val cacheMilli = cache[0]!!.lastUpdatedInMilli + val ageMillis = System.currentTimeMillis() - cacheMilli + val ageMinutes = TimeUnit.MILLISECONDS.toMinutes(ageMillis) + + val tooEarly = isManualRefresh && ageMinutes < MANUAL_REFRESH_MINUTES + val maxAge = if (isManualRefresh) MANUAL_REFRESH_MINUTES else AUTO_REFRESH_MAX_MINUTES + + if (tooEarly) return AlertResultType.RETURN_CACHE + + val shouldReturnCache = ageMinutes < maxAge + + return if (shouldReturnCache) AlertResultType.RETURN_CACHE else AlertResultType.ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt new file mode 100644 index 000000000..7a207bf2b --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt @@ -0,0 +1,31 @@ +package com.pranshulgg.weather_master_app.data.local.dao.alerts + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.pranshulgg.weather_master_app.data.local.entity.airquality.CurrentAirQualityEntity +import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity + + +@Dao +interface AlertsDao { + + @Transaction + suspend fun insertAlerts(alerts: List, locationId: String) { + deleteAlertsForLocation(locationId) + insertAlertList(alerts) + } + + + @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) + suspend fun insertAlertList(alerts: List) + + @Query("SELECT * FROM alerts WHERE locationId = :locationId") + suspend fun getAlertsForLocation(locationId: String): List + + @Query("DELETE FROM alerts WHERE locationId = :locationId") + suspend fun deleteAlertsForLocation(locationId: String) + +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/alerts/AlertEntity.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/alerts/AlertEntity.kt new file mode 100644 index 000000000..e72772fb0 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/alerts/AlertEntity.kt @@ -0,0 +1,34 @@ +package com.pranshulgg.weather_master_app.data.local.entity.alerts + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity +import com.pranshulgg.weather_master_app.data.local.entity.location.WeatherLocationEntity + + +@Entity( + tableName = "alerts", + foreignKeys = [ + ForeignKey( + entity = WeatherLocationEntity::class, + parentColumns = ["id"], + childColumns = ["locationId"], + onDelete = ForeignKey.Companion.CASCADE + ) + ], + indices = [Index("locationId")], +) +data class AlertEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val locationId: String, + + val event: String, + val severity: AlertSeverity?, + val effective: Long, + val expires: Long, + val description: String, + val source: String?, + val lastUpdatedInMilli: Long +) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/AlertDomainMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/AlertDomainMapper.kt new file mode 100644 index 000000000..23f4b7dd8 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/AlertDomainMapper.kt @@ -0,0 +1,31 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.alerts + +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert +import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity + + +fun Alert.toEntity(locationId: String): AlertEntity { + return AlertEntity( + locationId = locationId, + event = event, + severity = severity, + effective = effective, + expires = expires, + description = description, + source = source, + lastUpdatedInMilli = lastUpdatedInMilli + ) +} + +fun AlertEntity.toDomain(): Alert { + return Alert( + locationId = locationId, + event = event, + severity = severity, + effective = effective, + expires = expires, + description = description, + source = source, + lastUpdatedInMilli = lastUpdatedInMilli + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/accu/AccuAlertsMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/accu/AccuAlertsMapper.kt new file mode 100644 index 000000000..4b90ed735 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/accu/AccuAlertsMapper.kt @@ -0,0 +1,29 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.alerts.sources.accu + +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity +import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.json.AlertsAccuJson +import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.secondsToMilliseconds + + +fun AlertsAccuJson.toDomain(locationId: String): Alert { + return Alert( + locationId = locationId, + event = event.english, + severity = getSeverity(alarmLevel), + effective = area[0].epochStartTimeSeconds.secondsToMilliseconds(), + expires = area[0].epochEndTimeSeconds.secondsToMilliseconds(), + description = area[0].text, + source = source, + lastUpdatedInMilli = System.currentTimeMillis() + ) +} + +private fun getSeverity(severity: String?): AlertSeverity { + return when (severity) { + "Red" -> AlertSeverity.CRITICAL + "Orange" -> AlertSeverity.HIGH + "Yellow" -> AlertSeverity.MODERATE + else -> AlertSeverity.UNKNOWN + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt new file mode 100644 index 000000000..679336812 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt @@ -0,0 +1,21 @@ +package com.pranshulgg.weather_master_app.data.provider + +import com.pranshulgg.weather_master_app.core.model.sources.AlertSource +import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuRepository +import com.pranshulgg.weather_master_app.data.repository.AlertRepository +import javax.inject.Inject + + +class AlertsRepositoryProvider @Inject constructor( + private val alertsAccuRepository: AlertsAccuRepository +) { + + fun getRepository(source: AlertSource): AlertRepository? { + return when (source) { + AlertSource.ACCU_WEATHER -> alertsAccuRepository + AlertSource.WEATHER_API -> alertsAccuRepository + AlertSource.NONE -> null + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AlertRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AlertRepository.kt new file mode 100644 index 000000000..f63897963 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AlertRepository.kt @@ -0,0 +1,12 @@ +package com.pranshulgg.weather_master_app.data.repository + +import com.pranshulgg.weather_master_app.core.model.domain.location.Location +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult + +interface AlertRepository { + suspend fun getAlerts( + location: Location, + isManualRefresh: Boolean = false, + isForceRefresh: Boolean + ): AlertResult +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt new file mode 100644 index 000000000..224c65618 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt @@ -0,0 +1,71 @@ +package com.pranshulgg.weather_master_app.feature.main.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.pranshulgg.weather_master_app.R +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity +import com.pranshulgg.weather_master_app.core.prefs.AppPrefs +import com.pranshulgg.weather_master_app.core.prefs.AppPrefsState +import com.pranshulgg.weather_master_app.core.ui.components.SettingSection +import com.pranshulgg.weather_master_app.core.ui.components.Symbol +import com.pranshulgg.weather_master_app.core.ui.theme.ShadowElevation +import com.pranshulgg.weather_master_app.core.utils.formatters.safeZoneId +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Composable +fun AlertsSection(alerts: List, prefs: AppPrefsState, zoneId: String) { + + val pattern = if (prefs.is24HrTimeFormat) "MMM dd, HH:mm" else "MMM dd, hh:mm a" + + + val formatter: (Long) -> String = { + val formatter = DateTimeFormatter.ofPattern(pattern) + val instant = Instant.ofEpochMilli(it) + val dateTime = instant.atZone(safeZoneId(zoneId)).toLocalDateTime() + formatter.format(dateTime) + } + + Surface( + color = MaterialTheme.colorScheme.surface, + shape = MaterialTheme.shapes.extraLarge, + shadowElevation = ShadowElevation.level2 + ) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { + alerts.forEach { + ListItem( + colors = ListItemDefaults.colors( + containerColor = Color.Transparent + ), + leadingContent = { + Symbol( + R.drawable.warning_24px, + color = it.severity?.color ?: AlertSeverity.UNKNOWN.color, + size = 32.dp + ) + }, + headlineContent = { Text(it.event) }, + supportingContent = { + Text("${formatter(it.effective)} • ${formatter(it.expires)}") + } + ) + } + } + + } +} \ No newline at end of file From c2739dee85d92df9c5519ddfa66e3500257e18ca Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:30:58 +0530 Subject: [PATCH 19/40] weather alerts --- .../weather_master_app/core/di/AppModule.kt | 6 ++++-- .../weather_master_app/core/di/db/DaoModule.kt | 3 +++ .../weather_master_app/core/di/network/ApiModule.kt | 5 +++++ .../core/di/weather/WeatherRepositoryModule.kt | 13 +++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/AppModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/AppModule.kt index 01708f368..6db201301 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/AppModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/AppModule.kt @@ -24,6 +24,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.openmeteo. import com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao +import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao import com.pranshulgg.weather_master_app.data.local.dao.github.GithubDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherBlocksDao @@ -82,9 +83,10 @@ object AppModule { nwsDao: NwsDao, locationsDao: LocationsDao, accuDao: AccuDao, - airQualityDao: AirQualityDao + airQualityDao: AirQualityDao, + alertsDao: AlertsDao ): WeatherDataReconcilerRepository = - WeatherDataReconcilerRepository(nwsDao, locationsDao, accuDao, airQualityDao) + WeatherDataReconcilerRepository(nwsDao, locationsDao, accuDao, airQualityDao, alertsDao) @Provides @Singleton diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/db/DaoModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/db/DaoModule.kt index e86622c71..a7668206d 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/db/DaoModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/db/DaoModule.kt @@ -37,4 +37,7 @@ object DaoModule { @Provides fun provideAccuDao(db: WeatherMasterDatabase) = db.accuDao() + + @Provides + fun provideAlertsDao(db: WeatherMasterDatabase) = db.alertsDao() } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt index e953b97d8..05fad32bf 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt @@ -4,6 +4,7 @@ import com.pranshulgg.weather_master_app.core.network.github.GithubApi import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim.NominatimApi import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiApi import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi +import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuApi import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.GeoNamesSearchApi import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.timezone.GeoNamesTimezoneApi import com.pranshulgg.weather_master_app.core.network.sources.search.openmeteo.OpenMeteoSearchApi @@ -104,4 +105,8 @@ object ApiModule { @Provides @Singleton fun provideAccuAqiApi(): AccuAqiApi = AccuAqiApi.create() + + @Provides + @Singleton + fun provideAlertsAccuApi(): AlertsAccuApi = AlertsAccuApi.create() } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt index d01562d13..3cfdcd7ea 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt @@ -4,6 +4,8 @@ import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.Ac import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiRepository import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository +import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuApi +import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuRepository import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuApi import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuRepository import com.pranshulgg.weather_master_app.core.network.sources.weather.bmkg.BmkgApi @@ -30,6 +32,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.smhi.SmhiA import com.pranshulgg.weather_master_app.core.network.sources.weather.smhi.SmhiRepository import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao +import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherDao import com.pranshulgg.weather_master_app.data.local.dao.weather.nws.NwsDao @@ -156,4 +159,14 @@ object WeatherRepositoryModule { accuDao: AccuDao, accuApi: AccuApi ): AccuAqiRepository = AccuAqiRepository(api, dao, accuDao, accuApi) + + @Provides + @Singleton + fun provideAccuAlertsRepository( + api: AlertsAccuApi, + dao: AlertsDao, + accuDao: AccuDao, + accuApi: AccuApi + ): AlertsAccuRepository = AlertsAccuRepository(api, dao, accuDao, accuApi) + } \ No newline at end of file From 81b3caef4a50c93bc5e6672dd193e1899ee8a566 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:31:17 +0530 Subject: [PATCH 20/40] weather alerts --- .../core/ui/navigation/AppNavHost.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt index 5e8ceae51..075515d01 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt @@ -13,8 +13,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -45,6 +47,7 @@ import com.pranshulgg.weather_master_app.feature.settings.background.WorkerInfoS import com.pranshulgg.weather_master_app.feature.settings.language.LanguageScreen import com.pranshulgg.weather_master_app.feature.settings.sources.WeatherSourcesScreen import com.pranshulgg.weather_master_app.feature.settings.units.UnitsScreen +import com.pranshulgg.weather_master_app.feature.shared.WeatherViewModel @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -72,8 +75,13 @@ fun AppNavHost( ) { composable( NavRoutes.MAIN - ) { - MainScreen(navController) + ) { backStackEntry -> + + val rootEntry = remember(backStackEntry) { + navController.getBackStackEntry("root") + } + val weatherViewModel: WeatherViewModel = hiltViewModel(rootEntry) + MainScreen(navController, weatherViewModel) } composable( NavRoutes.SEARCH @@ -164,7 +172,12 @@ fun AppNavHost( val locationId = backStackEntry.arguments?.getString("locationId") - EditLocationScreen(navController, locationId!!) + val rootEntry = remember(backStackEntry) { + navController.getBackStackEntry("root") + } + val weatherViewModel: WeatherViewModel = hiltViewModel(rootEntry) + + EditLocationScreen(navController, locationId!!, weatherViewModel) } composable( route = "{block}/{index}/{locationId}", From 4f79efb80c12c2232ff9af76b860555bb9ec0e2b Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:31:22 +0530 Subject: [PATCH 21/40] weather alerts --- .../feature/main/MainScreen.kt | 23 ++++--------------- .../feature/main/MainScreenScaffold.kt | 10 ++++++++ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreen.kt index 0f369b664..149b24d15 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreen.kt @@ -24,6 +24,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import com.pranshulgg.weather_master_app.R import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQuality +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert import com.pranshulgg.weather_master_app.core.model.domain.location.Location import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherBlock @@ -51,7 +52,8 @@ data class MainScreenWeatherUiState( val isInitialized: Boolean = false, val airQuality: AirQuality? = null, val isAirQualityLoading: Boolean = false, - val isUnsupportedSource: Boolean = false + val isUnsupportedSource: Boolean = false, + val alerts: List = emptyList() ) data class MainScreenUiState( @@ -146,7 +148,8 @@ fun MainScreen(navController: NavController, weatherViewModel: WeatherViewModel) weatherViewModel.setActiveLocation(it) } }, - isTabletLike = isTabletLike + isTabletLike = isTabletLike, + weatherViewModel ) }, drawerState = drawerState, @@ -166,7 +169,6 @@ fun MainScreen(navController: NavController, weatherViewModel: WeatherViewModel) } }, onEditLocation = { -// viewModel.showWeatherSourcesForLocationSheet(uiState.isLoading) navController.navigate(NavRoutes.editLocation(activeLocation!!.id)) }, context, @@ -177,21 +179,6 @@ fun MainScreen(navController: NavController, weatherViewModel: WeatherViewModel) ) - // WEATHER SOURCES DIALOG -// SharedBottomSheet.WeatherSourcesForLocationSheet( -// countryCode = activeLocation?.countryCode, -// show = viewModel.uiState.value.isWeatherSourcesForLocationSheetOpen, -// isEditing = true, -// selectedSource = activeLocation?.source ?: WeatherSource.OPEN_METEO, -// onSave = { -// if (activeLocation != null) { -// weatherViewModel.updateSourceForLocation(activeLocation, it) -// } -// }, -// onDismiss = viewModel::hideWeatherSourcesForLocationSheet, -// sheetState = sheetState -// ) - // WEATHER SOURCES INFO DIALOG MainScreenBottomSheets.WeatherSourcesInfoForLocationSheet(viewModel, activeLocation, sheetState) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreenScaffold.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreenScaffold.kt index b041af82a..e5f9dccb3 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreenScaffold.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreenScaffold.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.DrawerState import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.LoadingIndicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -34,6 +35,7 @@ import com.pranshulgg.weather_master_app.core.prefs.LocalAppPrefs import com.pranshulgg.weather_master_app.feature.main.components.CreditsBottomSection import com.pranshulgg.weather_master_app.feature.main.components.FroggyContainer import com.pranshulgg.weather_master_app.feature.main.components.MainSearchBar +import com.pranshulgg.weather_master_app.feature.main.ui.AlertsSection import com.pranshulgg.weather_master_app.feature.main.ui.BackgroundGradient import com.pranshulgg.weather_master_app.feature.main.ui.CurrentWeatherCard import com.pranshulgg.weather_master_app.feature.main.ui.weatherAnimations.WeatherAnimations @@ -60,6 +62,7 @@ fun MainScreenScaffold( val weather = remember(uiState.weather) { uiState.weather } val airQuality = remember(uiState.airQuality) { uiState.airQuality } val prefs = LocalAppPrefs.current + val alerts = remember(uiState.alerts) { uiState.alerts } val units = uiState.weatherUnits val scrollState = rememberScrollState() @@ -156,6 +159,13 @@ fun MainScreenScaffold( ), verticalArrangement = Arrangement.spacedBy(14.dp) ) { + if (alerts.isNotEmpty()) { + AlertsSection( + alerts, + prefs, + weather.location.timezone + ) + } if (isShowSummary) { SummaryCard( weather, From 3290e68c635ffb37557103455d32cf39f7299189 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:31:26 +0530 Subject: [PATCH 22/40] weather alerts --- .../WeatherDataReconcilerRepository.kt | 44 +++++++++-- .../feature/shared/WeatherViewModel.kt | 74 +++++++++++++++++-- 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt index 77e5719ff..de41351e6 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt @@ -1,9 +1,11 @@ package com.pranshulgg.weather_master_app.data.repository import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource +import com.pranshulgg.weather_master_app.core.model.sources.AlertSource import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao +import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao import com.pranshulgg.weather_master_app.data.local.dao.weather.nws.NwsDao import jakarta.inject.Inject @@ -13,7 +15,8 @@ class WeatherDataReconcilerRepository @Inject constructor( private val nwsDao: NwsDao, private val locationDao: LocationsDao, private val accuDao: AccuDao, - private val airQualityDao: AirQualityDao + private val airQualityDao: AirQualityDao, + private val alertsDao: AlertsDao ) { /** @@ -25,32 +28,57 @@ class WeatherDataReconcilerRepository @Inject constructor( suspend fun cleanUpStaleData( previousSource: WeatherSource, locationId: String, - airQualitySource: AirQualitySource + airQualitySource: AirQualitySource, + currentAlertSource: AlertSource ) { when (previousSource) { WeatherSource.NWS -> nwsDao.deleteGridPointsForLocation(locationId) - WeatherSource.ACCU_WEATHER -> cleanAccuWeather(locationId, airQualitySource) + WeatherSource.ACCU_WEATHER -> cleanAccuWeather( + locationId, + airQualitySource, + currentAlertSource + ) + else -> {} } } - suspend fun cleanAccuWeather(locationId: String, airQualitySource: AirQualitySource) { - if (airQualitySource != AirQualitySource.ACCU_WEATHER) { + suspend fun cleanAccuWeather( + locationId: String, airQualitySource: AirQualitySource, + currentAlertSource: AlertSource + ) { + if (airQualitySource != AirQualitySource.ACCU_WEATHER || currentAlertSource != AlertSource.ACCU_WEATHER) { accuDao.deleteCityKeyForLocation(locationId) } } - suspend fun cleanUpStateAirQualityData( + suspend fun cleanUpStaleAirQualityData( currentSource: AirQualitySource, locationId: String, - currentWeatherSource: WeatherSource + currentWeatherSource: WeatherSource, + currentAlertSource: AlertSource ) { if (currentSource == AirQualitySource.NONE) { airQualityDao.deleteCurrentAirQuality(locationId) airQualityDao.deleteHourlyAirQuality(locationId) } - if (currentWeatherSource != WeatherSource.ACCU_WEATHER) { + if (currentWeatherSource != WeatherSource.ACCU_WEATHER || currentAlertSource != AlertSource.ACCU_WEATHER) { + accuDao.deleteCityKeyForLocation(locationId) + } + } + + suspend fun cleanUpStaleAlertsData( + currentSource: AlertSource, + locationId: String, + currentWeatherSource: WeatherSource, + airQualitySource: AirQualitySource, + ) { + if (currentSource == AlertSource.NONE) { + alertsDao.deleteAlertsForLocation(locationId) + } + + if (currentWeatherSource != WeatherSource.ACCU_WEATHER || airQualitySource != AirQualitySource.ACCU_WEATHER) { accuDao.deleteCityKeyForLocation(locationId) } } diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt index 59ef96fc2..c1a481d88 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt @@ -12,14 +12,17 @@ import com.pranshulgg.weather_master_app.core.model.domain.toAppException import com.pranshulgg.weather_master_app.core.model.domain.toMessageRes import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherBlock import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource +import com.pranshulgg.weather_master_app.core.model.sources.AlertSource import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource import com.pranshulgg.weather_master_app.core.model.sources.isGlobal import com.pranshulgg.weather_master_app.core.model.sources.isSourceSupportedFor import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResult +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager import com.pranshulgg.weather_master_app.data.provider.AirQualityRepositoryProvider +import com.pranshulgg.weather_master_app.data.provider.AlertsRepositoryProvider import com.pranshulgg.weather_master_app.data.provider.WeatherRepositoryProvider import com.pranshulgg.weather_master_app.data.repository.LocationsRepository import com.pranshulgg.weather_master_app.data.repository.WeatherBlocksRepository @@ -49,6 +52,7 @@ class WeatherViewModel @Inject constructor( private val openMeteoAqiRepository: OpenMeteoAqiRepository, private val weatherDataReconcilerRepository: WeatherDataReconcilerRepository, private val airQualityRepositoryProvider: AirQualityRepositoryProvider, + private val alertRepositoryProvider: AlertsRepositoryProvider, @ApplicationContext private val context: Context ) : ViewModel() { @@ -112,7 +116,8 @@ class WeatherViewModel @Inject constructor( source: WeatherSource, isManualRefresh: Boolean = false, isForceRefresh: Boolean = false, - isForceRefreshForAirQuality: Boolean = false + isForceRefreshForAirQuality: Boolean = false, + isForceRefreshForAlerts: Boolean = false ) { setLoading(true) weatherJob?.cancel() @@ -131,6 +136,9 @@ class WeatherViewModel @Inject constructor( launch { handleAirQuality(location, isManualRefresh, isForceRefreshForAirQuality) } + launch { + handleAlerts(location, isManualRefresh, isForceRefreshForAlerts) + } } handleWeatherData(source, location, isManualRefresh, isForceRefresh) @@ -177,30 +185,47 @@ class WeatherViewModel @Inject constructor( location: Location, source: WeatherSource, airQualitySource: AirQualitySource, + alertSource: AlertSource ) { - val updatedLocation = location.copy(source = source, airQualitySource = airQualitySource) + val updatedLocation = location.copy( + source = source, + airQualitySource = airQualitySource, + alertSource = alertSource + ) viewModelScope.launch { locationsRepo.updateSourceForLocation(location.id, source) locationsRepo.updateAirQualitySourceForLocation(location.id, airQualitySource) + locationsRepo.updateAlertSourceForLocation(location.id, alertSource) val allowForceRefreshForWeather = location.source != source val allowForceRefreshForAirQuality = location.airQualitySource != airQualitySource + val allowForceRefreshForAlerts = location.alertSource != alertSource if (allowForceRefreshForWeather) { weatherDataReconcilerRepository.cleanUpStaleData( location.source, location.id, - airQualitySource + airQualitySource, + alertSource ) } if (allowForceRefreshForAirQuality) { - weatherDataReconcilerRepository.cleanUpStateAirQualityData( + weatherDataReconcilerRepository.cleanUpStaleAirQualityData( airQualitySource, location.id, - source + source, + alertSource + ) + } + if (allowForceRefreshForAlerts) { + weatherDataReconcilerRepository.cleanUpStaleAlertsData( + alertSource, + location.id, + source, + airQualitySource ) } _uiState.value = _uiState.value.copy( @@ -210,7 +235,8 @@ class WeatherViewModel @Inject constructor( updatedLocation, source, isForceRefresh = allowForceRefreshForWeather, - isForceRefreshForAirQuality = allowForceRefreshForAirQuality + isForceRefreshForAirQuality = allowForceRefreshForAirQuality, + isForceRefreshForAlerts = allowForceRefreshForAlerts ) @@ -334,10 +360,42 @@ class WeatherViewModel @Inject constructor( airQuality = result.cacheAirQuality, isAirQualityLoading = false ) - - } } } + private suspend fun handleAlerts( + location: Location, + isManualRefresh: Boolean, + isForceRefresh: Boolean + ) { + + + val repoAlert = alertRepositoryProvider.getRepository(location.alertSource) + + + if (repoAlert == null) { + _uiState.value = _uiState.value.copy(alerts = emptyList()) + return + } + + + when (val result = repoAlert.getAlerts(location, isManualRefresh, isForceRefresh)) { + is AlertResult.Success -> { + _uiState.value = + _uiState.value.copy( + alerts = result.alerts + ) + + + } + + // Fail silently, we just won't show the Air quality in the UI + is AlertResult.Error -> { + _uiState.value = _uiState.value.copy( + alerts = result.cacheAlerts + ) + } + } + } } From 1f350ce7ba2f204af060d49f0b28e57aeb568210 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:31:33 +0530 Subject: [PATCH 23/40] weather alerts --- .../data/local/WeatherMasterDatabase.kt | 12 +++++++++--- .../data/local/dao/location/LocationsDao.kt | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt index c701e3a2a..a8a2f4892 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt @@ -9,6 +9,7 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao +import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao import com.pranshulgg.weather_master_app.data.local.dao.github.GithubDao import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherUnitsDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao @@ -19,6 +20,7 @@ import com.pranshulgg.weather_master_app.data.local.entity.weather.units.AppWeat import com.pranshulgg.weather_master_app.data.local.entity.airquality.CurrentAirQualityEntity import com.pranshulgg.weather_master_app.data.local.entity.airquality.HourlyAirQualityEntity import com.pranshulgg.weather_master_app.data.local.entity.airquality.accu.AccuEntity +import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity import com.pranshulgg.weather_master_app.data.local.entity.github.GithubEntity import com.pranshulgg.weather_master_app.data.local.entity.weather.CurrentWeatherEntity import com.pranshulgg.weather_master_app.data.local.entity.weather.DailyWeatherEntity @@ -39,16 +41,18 @@ import com.pranshulgg.weather_master_app.data.local.entity.weather.nws.NwsGridPo NwsGridPointsEntity::class, GithubEntity::class, HourlyAirQualityEntity::class, - AccuEntity::class + AccuEntity::class, + AlertEntity::class ], - version = 49, + version = 50, autoMigrations = [ AutoMigration(from = 39, to = 40), AutoMigration(from = 42, to = 43), AutoMigration(from = 43, to = 44), AutoMigration(from = 44, to = 45), AutoMigration(from = 47, to = 48), - AutoMigration(from = 48, to = 49) + AutoMigration(from = 48, to = 49), + AutoMigration(from = 49, to = 50) ] ) abstract class WeatherMasterDatabase : RoomDatabase() { @@ -63,6 +67,8 @@ abstract class WeatherMasterDatabase : RoomDatabase() { abstract fun accuDao(): AccuDao + abstract fun alertsDao(): AlertsDao + companion object { @Volatile diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt index d92548754..03e3c60f5 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt @@ -6,7 +6,9 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource +import com.pranshulgg.weather_master_app.core.model.sources.AlertSource import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource +import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity import com.pranshulgg.weather_master_app.data.local.entity.location.WeatherLocationEntity import com.pranshulgg.weather_master_app.data.local.entity.weather.WeatherWithRelations import kotlinx.coroutines.flow.Flow @@ -80,6 +82,13 @@ interface LocationsDao { @Query("UPDATE weather_locations SET airQualitySource = :source WHERE id = :id") suspend fun updateAirQualitySourceForLocation(id: String, source: AirQualitySource) + @Query("UPDATE weather_locations SET alertSource = :source WHERE id = :id") + suspend fun updateAlertSourceForLocation(id: String, source: AlertSource) + + @Query("UPDATE weather_locations SET customName = :name WHERE id = :id") suspend fun updateLocationCustomName(id: String, name: String?) + + @Query("SELECT * FROM alerts") + fun getAllLocationsAlerts(): Flow> } \ No newline at end of file From 981838b48f1f8843dfc155346050d0b3086caf2c Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:31:37 +0530 Subject: [PATCH 24/40] weather alerts --- .../feature/editlocation/EditLocationScreen.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt index f25703013..6a75cf20b 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt @@ -188,7 +188,8 @@ fun EditLocationScreen( weatherViewModel.handleSourceChangeForWeather( uiState.location, uiState.selectedWeatherSource ?: uiState.location.source, - uiState.selectedAirQualitySource ?: uiState.location.airQualitySource + uiState.selectedAirQualitySource ?: uiState.location.airQualitySource, + uiState.selectedAlertSource ?: uiState.location.alertSource ) }, shapes = ButtonDefaults.shapes(), @@ -196,14 +197,14 @@ fun EditLocationScreen( ) { Symbol( R.drawable.check_24px, - color = MaterialTheme.colorScheme.onPrimaryContainer, + color = MaterialTheme.colorScheme.onPrimary, size = ButtonDefaults.iconSizeFor(btnSize) ) Gap(horizontal = ButtonDefaults.iconSpacingFor(btnSize)) Text( stringResource(R.string.action_save_changes), style = ButtonDefaults.textStyleFor(btnSize), - color = MaterialTheme.colorScheme.onPrimaryContainer + color = MaterialTheme.colorScheme.onPrimary ) } From 7a55055a9024a7273e960e7e360e1451575eac9e Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:31:44 +0530 Subject: [PATCH 25/40] weather alerts --- .../data/repository/LocationsRepository.kt | 12 ++++++++++++ .../feature/locations/LocationsScreen.kt | 12 ++++++++---- .../feature/locations/LocationsScreenContent.kt | 10 ++++++++-- .../feature/locations/LocationsScreenViewModel.kt | 6 ++++++ .../feature/shared/components/LocationItem.kt | 8 ++++++-- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt index 33d7080e0..3010103f9 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt @@ -5,14 +5,18 @@ import android.util.Log import androidx.room.Transaction import com.pranshulgg.weather_master_app.core.model.domain.AppException import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQuality +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert import com.pranshulgg.weather_master_app.core.model.domain.location.Location import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource +import com.pranshulgg.weather_master_app.core.model.sources.AlertSource import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim.json.NominatimRepository import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao +import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity import com.pranshulgg.weather_master_app.data.local.mapper.airquality.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toDomain import com.pranshulgg.weather_master_app.data.local.mapper.locations.toDomain import com.pranshulgg.weather_master_app.data.local.mapper.locations.toEntity import com.pranshulgg.weather_master_app.data.local.mapper.weather.toDomain @@ -79,6 +83,10 @@ class LocationsRepository @Inject constructor( dao.updateAirQualitySourceForLocation(id, source) } + suspend fun updateAlertSourceForLocation(id: String, source: AlertSource) { + dao.updateAlertSourceForLocation(id, source) + } + suspend fun getLocationForId(id: String): Location { return dao.getLocationForId(id).toDomain() } @@ -207,6 +215,10 @@ class LocationsRepository @Inject constructor( return dao.getAllLocationsCurrentWeather() .map { list -> list.map { it.toDomain() } } } + + fun getAllLocationsAlerts(): Flow> { + return dao.getAllLocationsAlerts().map { list -> list.map { it?.toDomain() } } + } } diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt index 40dab2782..790f71640 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt @@ -62,11 +62,12 @@ fun LocationsScreen( locations: List, activeLocation: Location?, onLocationSelect: (Location) -> Unit, - isTabletLike: Boolean = false -) { + isTabletLike: Boolean = false, + weatherViewModel: WeatherViewModel, + + ) { val viewModel: LocationsScreenViewModel = hiltViewModel() - val weatherViewModel: WeatherViewModel = hiltViewModel() val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val uiState = viewModel.uiState @@ -74,6 +75,8 @@ fun LocationsScreen( initialValue = emptyList() ) + val alertsForLocations by viewModel.allLocationsAlerts.collectAsStateWithLifecycle(initialValue = emptyList()) + var backgroundLocationPermissionInfoDialogOpen by remember { mutableStateOf(false) } var locationPermissionInfoDialogOpen by remember { mutableStateOf(false) } @@ -128,7 +131,8 @@ fun LocationsScreen( activeLocation = activeLocation, weatherForLocations, onAddCurrentLocation = { locationPermissionInfoDialogOpen = true }, - uiState.value.isDeviceLocationLoading + uiState.value.isDeviceLocationLoading, + alertsForLocations ) } } diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenContent.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenContent.kt index 55e697297..3ab04e48b 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenContent.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenContent.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.pranshulgg.weather_master_app.R +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert import com.pranshulgg.weather_master_app.core.model.weather.WeatherCondition import com.pranshulgg.weather_master_app.core.model.domain.location.Location import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather @@ -46,10 +47,12 @@ fun LocationsScreenContent( activeLocation: Location? = null, weatherForLocations: List = emptyList(), onAddCurrentLocation: () -> Unit, - isDeviceLocationLoading: Boolean + isDeviceLocationLoading: Boolean, + alerts: List ) { val weatherMap = weatherForLocations.associateBy { it.location.id } + val alertMap = alerts.groupBy { it?.locationId } val context = LocalContext.current @@ -80,6 +83,8 @@ fun LocationsScreenContent( itemsIndexed(locations, key = { _, item -> item.id }) { index, location -> val weather = weatherMap[location.id] + val alert = alertMap[location.id] ?: emptyList() + val icon = weather?.current?.weatherCondition ?: WeatherCondition.NO_CONDITION_FOUND @@ -129,7 +134,8 @@ fun LocationsScreenContent( onLongClick = { onLongClick(location) }, isDefault = location.isDefault, isDeviceLocation = location.isDeviceLocation, - shape = shape + shape = shape, + isAlertAvailable = alert.isNotEmpty() ) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenViewModel.kt index 77bff15ca..101da3a5e 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenViewModel.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenViewModel.kt @@ -29,6 +29,12 @@ class LocationsScreenViewModel @Inject constructor( initialValue = emptyList() ) + val allLocationsAlerts = locationsRepo.getAllLocationsAlerts().stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) + fun updateDefaultLocation(id: String) { viewModelScope.launch { locationsRepo.updateDefaultLocation(id) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/components/LocationItem.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/components/LocationItem.kt index f2b043306..9f259d507 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/components/LocationItem.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/components/LocationItem.kt @@ -35,11 +35,12 @@ fun LocationItem( isDefault: Boolean = false, onLongClick: () -> Unit, isDeviceLocation: Boolean = false, - shape: RoundedCornerShape + shape: RoundedCornerShape, + isAlertAvailable: Boolean = false ) { val containerColor = - if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceBright + if (isSelected) MaterialTheme.colorScheme.secondaryContainer else if (isAlertAvailable) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.surfaceBright val contentColor = if (isSelected) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurface @@ -92,6 +93,9 @@ fun LocationItem( if (isDeviceLocation) { Symbol(R.drawable.circle_circle_24px, color = contentColor) } + if (isAlertAvailable) { + Symbol(R.drawable.warning_24px, color = MaterialTheme.colorScheme.error) + } } ) } From 5cd825e0529f0964ab987986b899d97bc7a01ff1 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:32:04 +0530 Subject: [PATCH 26/40] Fixed null value crashes for NWS --- .../sources/weather/nws/json/NwsGridPointsJson.kt | 10 +++++----- .../mapper/weather/sources/nws/NwsJsonMapper.kt | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/nws/json/NwsGridPointsJson.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/nws/json/NwsGridPointsJson.kt index 7cb017c41..554df91e3 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/nws/json/NwsGridPointsJson.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/nws/json/NwsGridPointsJson.kt @@ -35,7 +35,7 @@ data class NwsGridPointDataQuantitativePrecipitationJson( data class NwsGridPointDataQuantitativePrecipitationValuesJson( val validTime: String, - val value: Double + val value: Double? ) data class NwsGridPointDataSnowfallAmountJson( @@ -44,7 +44,7 @@ data class NwsGridPointDataSnowfallAmountJson( data class NwsGridPointDataSnowfallAmountValuesJson( val validTime: String, - val value: Double + val value: Double? ) data class NwsGridPointDataMaxTemperatureJson( @@ -57,12 +57,12 @@ data class NwsGridPointDataMinTemperatureJson( data class NwsGridPointDataMaxTemperatureValuesJson( val validTime: String, - val value: Double + val value: Double? ) data class NwsGridPointDataMinTemperatureValuesJson( val validTime: String, - val value: Double + val value: Double? ) data class NwsGridPointDataVisibilityJson( @@ -71,5 +71,5 @@ data class NwsGridPointDataVisibilityJson( data class NwsGridPointDataVisibilityValuesJson( val validTime: String, - val value: Double + val value: Double? ) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/nws/NwsJsonMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/nws/NwsJsonMapper.kt index 4a096a133..8395c53b8 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/nws/NwsJsonMapper.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/nws/NwsJsonMapper.kt @@ -221,7 +221,7 @@ fun NwsWeatherJsonBundle.toDomain(location: Location): Weather { dawn = sunTimings[index].dawn ?: 0L, dusk = sunTimings[index].dusk ?: 0L, pressureMsl = null, - visibility = minVisibility.roundToInt(), + visibility = minVisibility?.roundToInt(), humidity = avgHumidity, dewPoint = avgDewPoint ) @@ -314,7 +314,7 @@ private fun matchingMinMaxTemperature( private fun getRainSum( - data: Map, + data: Map, time: Long ): Double { @@ -330,7 +330,7 @@ private fun getRainSum( } private fun getSnowfallSum( - data: Map, + data: Map, time: Long ): Double { @@ -346,16 +346,16 @@ private fun getSnowfallSum( } private fun getMinVisibility( - data: Map, + data: Map, time: Long -): Double { +): Double? { val startIndex = data.toList().indexOfFirst { it.first >= time }.takeIf { it != -1 } ?: 0 val data = data.toList().drop(maxOf(0, startIndex)) .take(WeatherSource.NWS.hourlyAggregationLimitHours) - val minVisibility = data.minOf { it.second ?: 0.0 } + val minVisibility = data.mapNotNull { it.second }.minOfOrNull { it } return minVisibility } From ff8af9bd73bb3980a7583322a001670dce21ae33 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:32:25 +0530 Subject: [PATCH 27/40] filter search results without timezone --- .../weather_master_app/feature/search/SearchScreen.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/search/SearchScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/search/SearchScreen.kt index 77e8eb629..abacea57b 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/search/SearchScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/search/SearchScreen.kt @@ -132,16 +132,15 @@ fun SearchScreen(navController: NavController) { SettingSection( tiles = results.map { SettingTile.ActionTile( - title = it.name, - description = "${if (it.state.isNotEmpty()) "${it.state}, " else ""}${it.country}", + description = "${if (it.state.isNotBlank()) "${it.state}, " else ""}${it.country}", trailing = { if (selectedLocationId == it.id) { LoadingIndicator() } }, onClick = { - if (selectedLocationId.isNotEmpty()) return@ActionTile + if (selectedLocationId.isNotBlank()) return@ActionTile selectedLocation = it viewModel.showWeatherSourcesForLocationSheet() } From b6381be71584b73e6262b2271367762aeb9be598 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Tue, 28 Jul 2026 16:33:07 +0530 Subject: [PATCH 28/40] new drawable --- .../50.json | 1004 +++++++++++++++++ app/src/main/res/drawable/warning_24px.xml | 10 + 2 files changed, 1014 insertions(+) create mode 100644 app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/50.json create mode 100644 app/src/main/res/drawable/warning_24px.xml diff --git a/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/50.json b/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/50.json new file mode 100644 index 000000000..a0e15c8c8 --- /dev/null +++ b/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/50.json @@ -0,0 +1,1004 @@ +{ + "formatVersion": 1, + "database": { + "version": 50, + "identityHash": "33435bfcf5e28bbd931524e1d6c87a09", + "entities": [ + { + "tableName": "weather_locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `country` TEXT NOT NULL, `lat` REAL NOT NULL, `lon` REAL NOT NULL, `timezone` TEXT NOT NULL, `source` TEXT NOT NULL, `state` TEXT, `isFavorite` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL, `countryCode` TEXT, `isDefault` INTEGER NOT NULL, `isDeviceLocation` INTEGER NOT NULL, `alertSource` TEXT NOT NULL, `airQualitySource` TEXT NOT NULL, `customName` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "country", + "columnName": "country", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lat", + "columnName": "lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lon", + "columnName": "lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT" + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPinned", + "columnName": "isPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countryCode", + "columnName": "countryCode", + "affinity": "TEXT" + }, + { + "fieldPath": "isDefault", + "columnName": "isDefault", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeviceLocation", + "columnName": "isDeviceLocation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alertSource", + "columnName": "alertSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "airQualitySource", + "columnName": "airQualitySource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "customName", + "columnName": "customName", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "weather_current", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperature` REAL, `humidity` REAL NOT NULL, `windSpeed` REAL, `windDirection` TEXT, `pressureMsl` REAL, `visibility` INTEGER, `cloudCover` REAL, `uvIndex` REAL, `weatherCondition` TEXT NOT NULL, `feelsLike` REAL, `time` INTEGER NOT NULL, `dewPoint` REAL, `utcOffsetSeconds` INTEGER, `lastUpdatedInMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudCover", + "columnName": "cloudCover", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndex", + "columnName": "uvIndex", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feelsLike", + "columnName": "feelsLike", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "utcOffsetSeconds", + "columnName": "utcOffsetSeconds", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdatedInMilli", + "columnName": "lastUpdatedInMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_weather_current_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_current_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_hourly", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperature` REAL, `windSpeed` REAL, `windDirection` TEXT, `rain` REAL NOT NULL, `snowfall` REAL, `uvIndex` REAL, `weatherCondition` TEXT NOT NULL, `pressureMsl` REAL, `visibility` INTEGER, `humidity` REAL, `dewPoint` REAL, `time` INTEGER NOT NULL, `precipitationProbability` INTEGER, PRIMARY KEY(`locationId`, `time`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL" + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snowfall", + "columnName": "snowfall", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndex", + "columnName": "uvIndex", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL" + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "precipitationProbability", + "columnName": "precipitationProbability", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId", + "time" + ] + }, + "indices": [ + { + "name": "index_weather_hourly_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_hourly_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_daily", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperatureMin` REAL, `temperatureMax` REAL, `windSpeed` REAL, `windDirection` TEXT, `rainSum` REAL NOT NULL, `snowfallSum` REAL, `uvIndexMax` REAL, `weatherCondition` TEXT NOT NULL, `time` INTEGER NOT NULL, `precipitationProbabilityMax` INTEGER, `pressureMsl` REAL, `visibility` INTEGER, `humidity` REAL, `dewPoint` REAL, `sunrise` INTEGER NOT NULL, `sunset` INTEGER NOT NULL, `moonrise` INTEGER NOT NULL, `moonset` INTEGER NOT NULL, `moonPhase` TEXT NOT NULL, `dawn` INTEGER NOT NULL, `dusk` INTEGER NOT NULL, PRIMARY KEY(`locationId`, `time`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperatureMin", + "columnName": "temperatureMin", + "affinity": "REAL" + }, + { + "fieldPath": "temperatureMax", + "columnName": "temperatureMax", + "affinity": "REAL" + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "rainSum", + "columnName": "rainSum", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snowfallSum", + "columnName": "snowfallSum", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndexMax", + "columnName": "uvIndexMax", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "precipitationProbabilityMax", + "columnName": "precipitationProbabilityMax", + "affinity": "INTEGER" + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL" + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "sunrise", + "columnName": "sunrise", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sunset", + "columnName": "sunset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonrise", + "columnName": "moonrise", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonset", + "columnName": "moonset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonPhase", + "columnName": "moonPhase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dawn", + "columnName": "dawn", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dusk", + "columnName": "dusk", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId", + "time" + ] + }, + "indices": [ + { + "name": "index_weather_daily_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_daily_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_units", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tempUnit` TEXT NOT NULL, `windUnit` TEXT NOT NULL, `distanceUnit` TEXT NOT NULL, `pressureUnit` TEXT NOT NULL, `precipitationUnit` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tempUnit", + "columnName": "tempUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "windUnit", + "columnName": "windUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "distanceUnit", + "columnName": "distanceUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pressureUnit", + "columnName": "pressureUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precipitationUnit", + "columnName": "precipitationUnit", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "weather_blocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isDaily` INTEGER NOT NULL, `type` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDaily", + "columnName": "isDaily", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "air_quality_current", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `usAqi` INTEGER, `pm10` REAL, `pm25` REAL, `carbonMonoxide` REAL, `nitrogenDioxide` REAL, `sulphurDioxide` REAL, `ozone` REAL, `lastUpdatedInMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usAqi", + "columnName": "usAqi", + "affinity": "INTEGER" + }, + { + "fieldPath": "pm10", + "columnName": "pm10", + "affinity": "REAL" + }, + { + "fieldPath": "pm25", + "columnName": "pm25", + "affinity": "REAL" + }, + { + "fieldPath": "carbonMonoxide", + "columnName": "carbonMonoxide", + "affinity": "REAL" + }, + { + "fieldPath": "nitrogenDioxide", + "columnName": "nitrogenDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "sulphurDioxide", + "columnName": "sulphurDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "ozone", + "columnName": "ozone", + "affinity": "REAL" + }, + { + "fieldPath": "lastUpdatedInMilli", + "columnName": "lastUpdatedInMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_air_quality_current_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_air_quality_current_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nws_location_gridpoints", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `officeId` TEXT NOT NULL, `gridX` INTEGER NOT NULL, `gridY` INTEGER NOT NULL, `stationIdentifier` TEXT NOT NULL, `lastUpdatedMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "officeId", + "columnName": "officeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gridX", + "columnName": "gridX", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gridY", + "columnName": "gridY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stationIdentifier", + "columnName": "stationIdentifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdatedMilli", + "columnName": "lastUpdatedMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_nws_location_gridpoints_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nws_location_gridpoints_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "github", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `currentTag` TEXT NOT NULL, `lastFetchedTag` TEXT NOT NULL, `lastFetchedTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentTag", + "columnName": "currentTag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchedTag", + "columnName": "lastFetchedTag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchedTime", + "columnName": "lastFetchedTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "air_quality_hourly", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `locationId` TEXT NOT NULL, `pm10` REAL, `pm25` REAL, `carbonMonoxide` REAL, `nitrogenDioxide` REAL, `sulphurDioxide` REAL, `ozone` REAL, `time` INTEGER NOT NULL, FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pm10", + "columnName": "pm10", + "affinity": "REAL" + }, + { + "fieldPath": "pm25", + "columnName": "pm25", + "affinity": "REAL" + }, + { + "fieldPath": "carbonMonoxide", + "columnName": "carbonMonoxide", + "affinity": "REAL" + }, + { + "fieldPath": "nitrogenDioxide", + "columnName": "nitrogenDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "sulphurDioxide", + "columnName": "sulphurDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "ozone", + "columnName": "ozone", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_air_quality_hourly_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_air_quality_hourly_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "accu_locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `cityKey` TEXT NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cityKey", + "columnName": "cityKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_accu_locations_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_accu_locations_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "alerts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `locationId` TEXT NOT NULL, `event` TEXT NOT NULL, `severity` TEXT, `effective` INTEGER NOT NULL, `expires` INTEGER NOT NULL, `description` TEXT NOT NULL, `source` TEXT, `lastUpdatedInMilli` INTEGER NOT NULL, FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "event", + "columnName": "event", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "severity", + "columnName": "severity", + "affinity": "TEXT" + }, + { + "fieldPath": "effective", + "columnName": "effective", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expires", + "columnName": "expires", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdatedInMilli", + "columnName": "lastUpdatedInMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_alerts_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_alerts_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '33435bfcf5e28bbd931524e1d6c87a09')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/warning_24px.xml b/app/src/main/res/drawable/warning_24px.xml new file mode 100644 index 000000000..b9a5fb55f --- /dev/null +++ b/app/src/main/res/drawable/warning_24px.xml @@ -0,0 +1,10 @@ + + + From 9b6e1a2613dcbac35aa32cf53c08934ce433e36e Mon Sep 17 00:00:00 2001 From: Arpit Agarwal <9121467+arpitagarwal1301@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:59:47 +0530 Subject: [PATCH 29/40] Fix visibility graph overflow --- .../screens/visibility/components/VisibilityHourlyCard.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/blocks/screens/visibility/components/VisibilityHourlyCard.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/blocks/screens/visibility/components/VisibilityHourlyCard.kt index 6f418b926..fe9e99b01 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/blocks/screens/visibility/components/VisibilityHourlyCard.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/blocks/screens/visibility/components/VisibilityHourlyCard.kt @@ -97,7 +97,7 @@ fun VisibilityHourlyCard( val percentage = ((item.visibility!!.minus(visibilityMin)) .div((visibilityMax - visibilityMin))).times(100.0) - val barHeight = max((percentage.div(100.0)).times(140.0), 5.0) + val barHeight = max((percentage.div(100.0)).times(140.0), 5.0).coerceAtMost(140.0) val visibilityKm = @@ -155,4 +155,4 @@ fun VisibilityHourlyCard( } } } -} \ No newline at end of file +} From fcbb52f26b6478f5413b9eada9f378d6748d59a9 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Wed, 29 Jul 2026 17:27:49 +0530 Subject: [PATCH 30/40] add weatherapi.com as alert source --- .../core/di/network/ApiModule.kt | 5 + .../di/weather/WeatherRepositoryModule.kt | 9 ++ .../core/model/domain/alerts/Alert.kt | 4 +- .../model/weather/alerts/AlertSeverity.kt | 18 ++- .../alerts/weatherapi/AlertsWeatherApi.kt | 55 ++++++++ .../weatherapi/AlertsWeatherApiRepository.kt | 57 ++++++++ .../weatherapi/json/AlertsWeatherApiJson.kt | 19 +++ .../data/local/dao/alerts/AlertsDao.kt | 4 + .../data/local/dao/location/LocationsDao.kt | 2 - .../data/local/entity/alerts/AlertEntity.kt | 4 +- .../weatherapi/WeatherApiAlertsMapper.kt | 35 +++++ .../data/provider/AlertsRepositoryProvider.kt | 6 +- .../data/repository/AlertsRepository.kt | 21 +++ .../feature/alerts/AlertsScreen.kt | 97 +++++++++++++ .../feature/alerts/AlertsScreenViewModel.kt | 36 +++++ .../feature/alerts/components/AlertCard.kt | 132 ++++++++++++++++++ .../feature/main/ui/AlertsSection.kt | 13 +- 17 files changed, 501 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/AlertsWeatherApi.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/AlertsWeatherApiRepository.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/json/AlertsWeatherApiJson.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/weatherapi/WeatherApiAlertsMapper.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AlertsRepository.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/AlertsScreen.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/AlertsScreenViewModel.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/components/AlertCard.kt diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt index 05fad32bf..1ceaa9c33 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt @@ -5,6 +5,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim. import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiApi import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuApi +import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApi import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.GeoNamesSearchApi import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.timezone.GeoNamesTimezoneApi import com.pranshulgg.weather_master_app.core.network.sources.search.openmeteo.OpenMeteoSearchApi @@ -109,4 +110,8 @@ object ApiModule { @Provides @Singleton fun provideAlertsAccuApi(): AlertsAccuApi = AlertsAccuApi.create() + + @Provides + @Singleton + fun provideAlertsWeatherApi(): AlertsWeatherApi = AlertsWeatherApi.create() } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt index 3cfdcd7ea..f39899489 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt @@ -6,6 +6,8 @@ import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmet import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuApi import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuRepository +import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApi +import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApiRepository import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuApi import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuRepository import com.pranshulgg.weather_master_app.core.network.sources.weather.bmkg.BmkgApi @@ -169,4 +171,11 @@ object WeatherRepositoryModule { accuApi: AccuApi ): AlertsAccuRepository = AlertsAccuRepository(api, dao, accuDao, accuApi) + @Provides + @Singleton + fun provideWeatherApiAlertsRepository( + api: AlertsWeatherApi, + dao: AlertsDao + ): AlertsWeatherApiRepository = AlertsWeatherApiRepository(api, dao) + } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/domain/alerts/Alert.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/domain/alerts/Alert.kt index 433394a4d..c036770db 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/domain/alerts/Alert.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/domain/alerts/Alert.kt @@ -7,8 +7,8 @@ data class Alert( val locationId: String, val event: String, val severity: AlertSeverity?, - val effective: Long, - val expires: Long, + val effective: Long?, + val expires: Long?, val description: String, val source: String?, val lastUpdatedInMilli: Long diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt index 7203f0f83..dde7aa0ff 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt @@ -5,26 +5,32 @@ import com.pranshulgg.weather_master_app.R enum class AlertSeverity( val label: Int, - val color: Color + val color: Color, + val contentColor: Color, ) { CRITICAL( R.string.alert_severity_critical, - Color(0xFFD32F2F) + Color(0xFFD32F2F), + Color(0xFFFFEBEE) ), HIGH( R.string.alert_severity_high, - Color(0xFFF57C00) + Color(0xFFF57C00), + Color(0xFFFFF4E0) ), MODERATE( R.string.alert_severity_medium, - Color(0xFFFBC02D) + Color(0xFFFBC02D), + Color(0xFF5C4300) ), LOW( R.string.alert_severity_low, - Color(0xFF388E3C) + Color(0xFF388E3C), + Color(0xFF0F3D0F) ), UNKNOWN( R.string.alert_severity_unknown, - Color(0xFF757575) + Color(0xFF757575), + Color(0xFFF3E5F5) ) } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/AlertsWeatherApi.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/AlertsWeatherApi.kt new file mode 100644 index 000000000..9a914d3cf --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/AlertsWeatherApi.kt @@ -0,0 +1,55 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi + +import com.pranshulgg.weather_master_app.BuildConfig +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Query +import java.util.concurrent.TimeUnit +import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.json.AlertsWeatherApiJson +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Response + + +interface AlertsWeatherApi { + + @GET("alerts.json") + suspend fun fetchAlerts( + @Query("q") query: String + ): Response + + + companion object { + const val BASE_URL = "https://api.weatherapi.com/v1/" + + fun create(): AlertsWeatherApi { + + val auth = Interceptor { chain -> + val original = chain.request() + + val newUrl = + original.url.newBuilder() + .addQueryParameter("key", BuildConfig.WEATHERAPI_KEY) + .build() + val request = original.newBuilder().url(newUrl).build() + + chain.proceed(request) + } + + val client = OkHttpClient.Builder() + .addInterceptor(auth) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(AlertsWeatherApi::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/AlertsWeatherApiRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/AlertsWeatherApiRepository.kt new file mode 100644 index 000000000..1e84ee596 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/AlertsWeatherApiRepository.kt @@ -0,0 +1,57 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi + +import com.pranshulgg.weather_master_app.core.model.domain.AppException +import com.pranshulgg.weather_master_app.core.model.domain.location.Location +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResultType +import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnAlertsCache +import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.sources.weatherapi.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toEntity +import com.pranshulgg.weather_master_app.data.repository.AlertRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + + +class AlertsWeatherApiRepository @Inject constructor( + private val api: AlertsWeatherApi, + private val dao: AlertsDao +) : AlertRepository { + override suspend fun getAlerts( + location: Location, + isManualRefresh: Boolean, + isForceRefresh: Boolean + ): AlertResult = withContext(Dispatchers.IO) { + + val cache = dao.getAlertsForLocation(location.id) + val shouldReturnCache = shouldReturnAlertsCache(cache, isManualRefresh, isForceRefresh) + + when (shouldReturnCache) { + AlertResultType.RETURN_CACHE -> return@withContext AlertResult.Success(cache.map { it!!.toDomain() }) + else -> {} + } + + return@withContext try { + + val response = api.fetchAlerts("${location.latitude},${location.longitude}") + val body = response.body() + ?: return@withContext AlertResult.Error(exception = AppException.Unknown()) + + val domain = body.toDomain(location.id) + + dao.insertAlerts( + domain.map { it.toEntity(location.id) }, + location.id + ) + + AlertResult.Success(domain) + + } catch (e: Exception) { + AlertResult.Error(exception = e, cacheAlerts = cache.map { it!!.toDomain() }) + } + + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/json/AlertsWeatherApiJson.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/json/AlertsWeatherApiJson.kt new file mode 100644 index 000000000..7ad584cbe --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/weatherapi/json/AlertsWeatherApiJson.kt @@ -0,0 +1,19 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.json + + +data class AlertsWeatherApiJson( + val alerts: AlertsWeatherApiAlertJson +) + +data class AlertsWeatherApiAlertJson( + val alert: List +) + +data class AlertsWeatherApiAlertItemJson( + val event: String, + val effective: String?, + val expires: String?, + val desc: String?, + val severity: String?, + val headline: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt index 7a207bf2b..ff22fdcf3 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt @@ -7,6 +7,7 @@ import androidx.room.Query import androidx.room.Transaction import com.pranshulgg.weather_master_app.data.local.entity.airquality.CurrentAirQualityEntity import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity +import kotlinx.coroutines.flow.Flow @Dao @@ -28,4 +29,7 @@ interface AlertsDao { @Query("DELETE FROM alerts WHERE locationId = :locationId") suspend fun deleteAlertsForLocation(locationId: String) + @Query("SELECT * FROM alerts") + fun getAllLocationsAlerts(): Flow> + } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt index 03e3c60f5..ba2001e3f 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/location/LocationsDao.kt @@ -89,6 +89,4 @@ interface LocationsDao { @Query("UPDATE weather_locations SET customName = :name WHERE id = :id") suspend fun updateLocationCustomName(id: String, name: String?) - @Query("SELECT * FROM alerts") - fun getAllLocationsAlerts(): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/alerts/AlertEntity.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/alerts/AlertEntity.kt index e72772fb0..d0bbfe988 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/alerts/AlertEntity.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/entity/alerts/AlertEntity.kt @@ -26,8 +26,8 @@ data class AlertEntity( val event: String, val severity: AlertSeverity?, - val effective: Long, - val expires: Long, + val effective: Long?, + val expires: Long?, val description: String, val source: String?, val lastUpdatedInMilli: Long diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/weatherapi/WeatherApiAlertsMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/weatherapi/WeatherApiAlertsMapper.kt new file mode 100644 index 000000000..0570bf56a --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/weatherapi/WeatherApiAlertsMapper.kt @@ -0,0 +1,35 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.alerts.sources.weatherapi + +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity +import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.json.AlertsWeatherApiJson +import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.iso8601TimestampToMilliseconds + + +fun AlertsWeatherApiJson.toDomain(locationId: String): List { + return this.alerts.alert.map { + + val desc = if (it.desc.isNullOrBlank()) it.headline else it.desc + + Alert( + locationId = locationId, + event = it.event, + severity = getSeverity(it.severity), + effective = it.effective?.iso8601TimestampToMilliseconds(), + expires = it.expires?.iso8601TimestampToMilliseconds(), + description = desc ?: "", + source = "WeatherApi.com", + lastUpdatedInMilli = System.currentTimeMillis() + ) + } +} + +private fun getSeverity(severity: String?): AlertSeverity { + return when (severity) { + "Extreme" -> AlertSeverity.CRITICAL + "Severe" -> AlertSeverity.HIGH + "Moderate" -> AlertSeverity.MODERATE + "Minor" -> AlertSeverity.LOW + else -> AlertSeverity.UNKNOWN + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt index 679336812..59a657fda 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt @@ -2,18 +2,20 @@ package com.pranshulgg.weather_master_app.data.provider import com.pranshulgg.weather_master_app.core.model.sources.AlertSource import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuRepository +import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApiRepository import com.pranshulgg.weather_master_app.data.repository.AlertRepository import javax.inject.Inject class AlertsRepositoryProvider @Inject constructor( - private val alertsAccuRepository: AlertsAccuRepository + private val alertsAccuRepository: AlertsAccuRepository, + private val alertsWeatherApiRepository: AlertsWeatherApiRepository ) { fun getRepository(source: AlertSource): AlertRepository? { return when (source) { AlertSource.ACCU_WEATHER -> alertsAccuRepository - AlertSource.WEATHER_API -> alertsAccuRepository + AlertSource.WEATHER_API -> alertsWeatherApiRepository AlertSource.NONE -> null } } diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AlertsRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AlertsRepository.kt new file mode 100644 index 000000000..0dc41b40a --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/AlertsRepository.kt @@ -0,0 +1,21 @@ +package com.pranshulgg.weather_master_app.data.repository + +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert +import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toDomain +import jakarta.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + + +class AlertsRepository @Inject constructor( + private val dao: AlertsDao, +) { + fun getAllLocationsAlerts(): Flow> { + return dao.getAllLocationsAlerts().map { list -> list.map { it?.toDomain() } } + } + + suspend fun getAlertsForLocation(locationId: String): List { + return dao.getAlertsForLocation(locationId).map { it?.toDomain() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/AlertsScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/AlertsScreen.kt new file mode 100644 index 000000000..7fc659db6 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/AlertsScreen.kt @@ -0,0 +1,97 @@ +package com.pranshulgg.weather_master_app.feature.alerts + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavController +import com.pranshulgg.weather_master_app.R +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert +import com.pranshulgg.weather_master_app.core.model.domain.location.Location +import com.pranshulgg.weather_master_app.core.prefs.AppPrefs +import com.pranshulgg.weather_master_app.core.prefs.LocalAppPrefs +import com.pranshulgg.weather_master_app.core.ui.components.Gap +import com.pranshulgg.weather_master_app.core.ui.components.LargeTopBarScaffold +import com.pranshulgg.weather_master_app.core.ui.components.NavigateUpBtn +import com.pranshulgg.weather_master_app.core.ui.components.SettingSection +import com.pranshulgg.weather_master_app.core.ui.components.SettingTile +import com.pranshulgg.weather_master_app.core.ui.theme.ShapeRadius +import com.pranshulgg.weather_master_app.feature.alerts.components.AlertCard +import com.pranshulgg.weather_master_app.feature.settings.SettingsScreen + +data class AlertsScreenUiState( + val alerts: List = emptyList(), + val location: Location? = null +) + +@Composable +fun AlertsScreen(navController: NavController, locationId: String) { + + val viewModel: AlertsScreenViewModel = hiltViewModel() + val uiState = viewModel.uiState.value + val prefs = LocalAppPrefs.current + + LaunchedEffect(Unit) { + viewModel.getAlertsForLocation(locationId) + viewModel.getLocation(locationId) + } + + + LargeTopBarScaffold( + title = "Alerts", + navigationIcon = { NavigateUpBtn(navController) }, + ) { paddingValues -> + Column( + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(top = paddingValues.calculateTopPadding(), start = 16.dp, end = 16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + uiState.alerts.filterNotNull().forEachIndexed { index, it -> + val isFirst = index == 0 + val isLast = index == uiState.alerts.lastIndex + val isOnly = uiState.alerts.size == 1 + val shape = when { + isOnly -> RoundedCornerShape(ShapeRadius.Large) + isFirst -> RoundedCornerShape( + topStart = ShapeRadius.Large, + topEnd = ShapeRadius.Large, + bottomStart = ShapeRadius.ExtraSmall, + bottomEnd = ShapeRadius.ExtraSmall + ) + + isLast -> RoundedCornerShape( + topStart = ShapeRadius.ExtraSmall, + topEnd = ShapeRadius.ExtraSmall, + bottomStart = ShapeRadius.Large, + bottomEnd = ShapeRadius.Large + ) + + else -> RoundedCornerShape(ShapeRadius.ExtraSmall) + } + + + AlertCard(it, prefs, uiState.location!!.timezone, shape) + } + + Gap(WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + 30.dp) + + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/AlertsScreenViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/AlertsScreenViewModel.kt new file mode 100644 index 000000000..2465a6ce2 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/AlertsScreenViewModel.kt @@ -0,0 +1,36 @@ +package com.pranshulgg.weather_master_app.feature.alerts + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pranshulgg.weather_master_app.data.repository.AlertsRepository +import com.pranshulgg.weather_master_app.data.repository.LocationsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel + +class AlertsScreenViewModel @Inject constructor( + val alertsRepository: AlertsRepository, + val locationRepository: LocationsRepository +) : ViewModel() { + private var _uiState = mutableStateOf(AlertsScreenUiState()) + val uiState: State = _uiState + + fun getAlertsForLocation(locationId: String) { + viewModelScope.launch { + val alerts = alertsRepository.getAlertsForLocation(locationId) + _uiState.value = _uiState.value.copy(alerts = alerts) + } + } + + fun getLocation(locationId: String) { + viewModelScope.launch { + val location = locationRepository.getLocationForId(locationId) + _uiState.value = _uiState.value.copy(location = location) + } + } +} diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/components/AlertCard.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/components/AlertCard.kt new file mode 100644 index 000000000..00b83d11b --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/alerts/components/AlertCard.kt @@ -0,0 +1,132 @@ +package com.pranshulgg.weather_master_app.feature.alerts.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.pranshulgg.weather_master_app.R +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity +import com.pranshulgg.weather_master_app.core.prefs.AppPrefsState +import com.pranshulgg.weather_master_app.core.ui.components.Gap +import com.pranshulgg.weather_master_app.core.ui.components.Symbol +import com.pranshulgg.weather_master_app.core.ui.theme.ShadowElevation +import com.pranshulgg.weather_master_app.core.utils.formatters.safeZoneId +import java.time.Instant +import java.time.format.DateTimeFormatter + + +@Composable +fun AlertCard(alert: Alert, prefs: AppPrefsState, zoneId: String, shape: Shape) { + val pattern = if (prefs.is24HrTimeFormat) "MMM dd, HH:mm" else "MMM dd, hh:mm a" + + val formatter: (Long?) -> String? = { + it?.let { + val formatter = DateTimeFormatter.ofPattern(pattern) + val instant = Instant.ofEpochMilli(it) + val dateTime = instant.atZone(safeZoneId(zoneId)).toLocalDateTime() + formatter.format(dateTime) + } ?: "Unknown" + } + + Surface( + color = MaterialTheme.colorScheme.surfaceBright, + shape = shape, + shadowElevation = ShadowElevation.level2 + ) { + Column( + modifier = Modifier + .padding(bottom = 16.dp) + .fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.spacedBy( + 5.dp, + alignment = Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) { + Column() { + Text( + alert.event, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Gap(6.dp) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalArrangement = Arrangement.spacedBy( + 5.dp, + alignment = Alignment.CenterVertically + ) + ) { + Chip( + stringResource(alert.severity?.label ?: AlertSeverity.UNKNOWN.label), + alert.severity?.color ?: AlertSeverity.UNKNOWN.color, + alert.severity?.contentColor ?: AlertSeverity.UNKNOWN.contentColor + ) + Chip( + "Effective ${formatter(alert.effective)}", + MaterialTheme.colorScheme.tertiaryContainer, + MaterialTheme.colorScheme.onTertiaryContainer + ) + Chip( + "Expires ${formatter(alert.expires)}", + MaterialTheme.colorScheme.primaryContainer, + MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + } + Gap(12.dp) + HorizontalDivider() + Gap(12.dp) + Text( + alert.description, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Gap(12.dp) + HorizontalDivider() + Gap(12.dp) + Text( + "Source ${alert.source}", + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } +} + +@Composable +private fun Chip(text: String, color: Color, textColor: Color) { + Surface( + color = color, + shape = CircleShape, + ) { + Text( + text, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), + style = MaterialTheme.typography.labelLarge, + color = textColor + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt index 224c65618..4264874c7 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt @@ -1,5 +1,6 @@ package com.pranshulgg.weather_master_app.feature.main.ui +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -26,7 +27,12 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter @Composable -fun AlertsSection(alerts: List, prefs: AppPrefsState, zoneId: String) { +fun AlertsSection( + alerts: List, + prefs: AppPrefsState, + zoneId: String, + onAlertClick: () -> Unit +) { val pattern = if (prefs.is24HrTimeFormat) "MMM dd, HH:mm" else "MMM dd, hh:mm a" @@ -52,6 +58,7 @@ fun AlertsSection(alerts: List, prefs: AppPrefsState, zoneId: String) { colors = ListItemDefaults.colors( containerColor = Color.Transparent ), + modifier = Modifier.clickable(onClick = onAlertClick), leadingContent = { Symbol( R.drawable.warning_24px, @@ -61,7 +68,9 @@ fun AlertsSection(alerts: List, prefs: AppPrefsState, zoneId: String) { }, headlineContent = { Text(it.event) }, supportingContent = { - Text("${formatter(it.effective)} • ${formatter(it.expires)}") + if (it.effective != null && it.expires != null) { + Text("${formatter(it.effective)} • ${formatter(it.expires)}") + } } ) } From df96aa2481f1da3d773eaa0d8b0616819c7df111 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Wed, 29 Jul 2026 17:28:03 +0530 Subject: [PATCH 31/40] alert screen --- .../core/ui/navigation/AppNavHost.kt | 15 ++++++++++++++- .../core/ui/navigation/NavRoutes.kt | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt index 075515d01..cb5cb72b1 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/AppNavHost.kt @@ -23,6 +23,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.navArgument +import com.pranshulgg.weather_master_app.feature.alerts.AlertsScreen import com.pranshulgg.weather_master_app.feature.blocks.screens.SunMoonScreen import com.pranshulgg.weather_master_app.feature.blocks.screens.airquality.AirQualityScreen import com.pranshulgg.weather_master_app.feature.blocks.screens.humidity.HumidityScreen @@ -71,7 +72,7 @@ fun AppNavHost( navigation( route = "root", startDestination = NavRoutes.MAIN -// startDestination = NavRoutes.editLocation("af673c89-72b7-4da9-865d-c26fdabeca07") +// startDestination = NavRoutes.alerts("5e8c6c88-e85a-4e76-8d0b-5d1eb2906acd") ) { composable( NavRoutes.MAIN @@ -130,6 +131,18 @@ fun AppNavHost( DailyScreen(navController, index, locationId!!) } + composable( + route = "${NavRoutes.ALERTS}/{locationId}", + arguments = listOf( + navArgument("locationId") { + type = NavType.StringType + } + ) + ) { backStackEntry -> + val locationId = backStackEntry.arguments?.getString("locationId") + + AlertsScreen(navController, locationId!!) + } composable( NavRoutes.ABOUT ) { diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/NavRoutes.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/NavRoutes.kt index f18a462d0..71a9443bd 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/NavRoutes.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/ui/navigation/NavRoutes.kt @@ -47,6 +47,12 @@ object NavRoutes { const val EDIT_LOCATION = "edit_location" + const val ALERTS = "alerts" + + fun alerts(locationId: String): String { + return "$ALERTS/$locationId" + } + fun editLocation(locationId: String): String { return "$EDIT_LOCATION/$locationId" } From 5c3d5dab77e51405daadab4e544da2fc77253352 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Wed, 29 Jul 2026 17:28:16 +0530 Subject: [PATCH 32/40] alerts --- .../data/repository/LocationsRepository.kt | 8 +------- .../feature/intro/IntroScreenViewModel.kt | 1 - .../feature/locations/LocationsScreenViewModel.kt | 6 ++++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt index 3010103f9..391195642 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt @@ -1,7 +1,6 @@ package com.pranshulgg.weather_master_app.data.repository import android.content.Context -import android.util.Log import androidx.room.Transaction import com.pranshulgg.weather_master_app.core.model.domain.AppException import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQuality @@ -14,7 +13,6 @@ import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim.json.NominatimRepository import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao -import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity import com.pranshulgg.weather_master_app.data.local.mapper.airquality.toDomain import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toDomain import com.pranshulgg.weather_master_app.data.local.mapper.locations.toDomain @@ -27,11 +25,9 @@ import com.pranshulgg.weather_master_app.feature.intro.toDomain import dagger.hilt.android.qualifiers.ApplicationContext import jakarta.inject.Inject import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext import java.time.ZoneId import kotlin.coroutines.resumeWithException @@ -216,9 +212,7 @@ class LocationsRepository @Inject constructor( .map { list -> list.map { it.toDomain() } } } - fun getAllLocationsAlerts(): Flow> { - return dao.getAllLocationsAlerts().map { list -> list.map { it?.toDomain() } } - } + } diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/intro/IntroScreenViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/intro/IntroScreenViewModel.kt index 5bbd689de..e152ffc22 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/intro/IntroScreenViewModel.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/intro/IntroScreenViewModel.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel - class IntroScreenViewModel @Inject constructor( val locationsRepo: LocationsRepository, @ApplicationContext private val context: Context, diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenViewModel.kt index 101da3a5e..4f17d397a 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenViewModel.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreenViewModel.kt @@ -10,6 +10,7 @@ import com.pranshulgg.weather_master_app.core.model.domain.AppException import com.pranshulgg.weather_master_app.core.model.domain.location.Location import com.pranshulgg.weather_master_app.core.model.domain.toMessageRes import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager +import com.pranshulgg.weather_master_app.data.repository.AlertsRepository import com.pranshulgg.weather_master_app.data.repository.LocationsRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException @@ -20,7 +21,8 @@ import javax.inject.Inject @HiltViewModel class LocationsScreenViewModel @Inject constructor( - private val locationsRepo: LocationsRepository + private val locationsRepo: LocationsRepository, + private val alertsRepository: AlertsRepository ) : ViewModel() { val allLocationsWeather = locationsRepo.getWeatherForAllLocations().stateIn( @@ -29,7 +31,7 @@ class LocationsScreenViewModel @Inject constructor( initialValue = emptyList() ) - val allLocationsAlerts = locationsRepo.getAllLocationsAlerts().stateIn( + val allLocationsAlerts = alertsRepository.getAllLocationsAlerts().stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() From da6715d9584b01362820bdeb7f3091fd5f79cdae Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Wed, 29 Jul 2026 17:28:46 +0530 Subject: [PATCH 33/40] fix data reconciler --- .../data/local/WeatherMasterDatabase.kt | 5 +++-- .../repository/WeatherDataReconcilerRepository.kt | 12 +++--------- .../feature/main/MainScreenScaffold.kt | 10 +++++++++- .../feature/shared/WeatherViewModel.kt | 2 -- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt index a8a2f4892..8741479ea 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt @@ -44,7 +44,7 @@ import com.pranshulgg.weather_master_app.data.local.entity.weather.nws.NwsGridPo AccuEntity::class, AlertEntity::class ], - version = 50, + version = 51, autoMigrations = [ AutoMigration(from = 39, to = 40), AutoMigration(from = 42, to = 43), @@ -52,7 +52,8 @@ import com.pranshulgg.weather_master_app.data.local.entity.weather.nws.NwsGridPo AutoMigration(from = 44, to = 45), AutoMigration(from = 47, to = 48), AutoMigration(from = 48, to = 49), - AutoMigration(from = 49, to = 50) + AutoMigration(from = 49, to = 50), + AutoMigration(from = 50, to = 51) ] ) abstract class WeatherMasterDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt index de41351e6..43fb9ba43 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/WeatherDataReconcilerRepository.kt @@ -53,15 +53,12 @@ class WeatherDataReconcilerRepository @Inject constructor( } suspend fun cleanUpStaleAirQualityData( - currentSource: AirQualitySource, locationId: String, currentWeatherSource: WeatherSource, currentAlertSource: AlertSource ) { - if (currentSource == AirQualitySource.NONE) { - airQualityDao.deleteCurrentAirQuality(locationId) - airQualityDao.deleteHourlyAirQuality(locationId) - } + airQualityDao.deleteCurrentAirQuality(locationId) + airQualityDao.deleteHourlyAirQuality(locationId) if (currentWeatherSource != WeatherSource.ACCU_WEATHER || currentAlertSource != AlertSource.ACCU_WEATHER) { accuDao.deleteCityKeyForLocation(locationId) @@ -69,14 +66,11 @@ class WeatherDataReconcilerRepository @Inject constructor( } suspend fun cleanUpStaleAlertsData( - currentSource: AlertSource, locationId: String, currentWeatherSource: WeatherSource, airQualitySource: AirQualitySource, ) { - if (currentSource == AlertSource.NONE) { - alertsDao.deleteAlertsForLocation(locationId) - } + alertsDao.deleteAlertsForLocation(locationId) if (currentWeatherSource != WeatherSource.ACCU_WEATHER || airQualitySource != AirQualitySource.ACCU_WEATHER) { accuDao.deleteCityKeyForLocation(locationId) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreenScaffold.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreenScaffold.kt index e5f9dccb3..0dd82c717 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreenScaffold.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/MainScreenScaffold.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController import com.pranshulgg.weather_master_app.core.prefs.LocalAppPrefs +import com.pranshulgg.weather_master_app.core.ui.navigation.NavRoutes import com.pranshulgg.weather_master_app.feature.main.components.CreditsBottomSection import com.pranshulgg.weather_master_app.feature.main.components.FroggyContainer import com.pranshulgg.weather_master_app.feature.main.components.MainSearchBar @@ -163,7 +164,14 @@ fun MainScreenScaffold( AlertsSection( alerts, prefs, - weather.location.timezone + weather.location.timezone, + onAlertClick = { + navController.navigate( + NavRoutes.alerts( + weather.location.id + ) + ) + } ) } if (isShowSummary) { diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt index c1a481d88..314517219 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/WeatherViewModel.kt @@ -214,7 +214,6 @@ class WeatherViewModel @Inject constructor( } if (allowForceRefreshForAirQuality) { weatherDataReconcilerRepository.cleanUpStaleAirQualityData( - airQualitySource, location.id, source, alertSource @@ -222,7 +221,6 @@ class WeatherViewModel @Inject constructor( } if (allowForceRefreshForAlerts) { weatherDataReconcilerRepository.cleanUpStaleAlertsData( - alertSource, location.id, source, airQualitySource From f8bcf885a8f0e783966bc70b55565511515fac16 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Wed, 29 Jul 2026 17:28:53 +0530 Subject: [PATCH 34/40] new api --- app/build.gradle.kts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 353c07af5..34d30cbd8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,6 +24,9 @@ val mfKey = providers.gradleProperty("MF_KEY").orNull ?: System.getenv("MF_KEY") ?: "" val accuKey = providers.gradleProperty("ACCU_KEY").orNull ?: System.getenv("ACCU_KEY") ?: "" +val weatherapiKey = + providers.gradleProperty("WEATHERAPI_KEY").orNull ?: System.getenv("WEATHERAPI_KEY") + ?: "" val keystoreFile = file("../keystore/release.jks") val hasKeystore = keystoreFile.exists() @@ -60,6 +63,12 @@ android { "ACCU_KEY", "\"$accuKey\"" ) + + buildConfigField( + "String", + "WEATHERAPI_KEY", + "\"$weatherapiKey\"" + ) } signingConfigs { From 05d0749c775bbdcaff842217c8f07e3c6cb98740 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Wed, 29 Jul 2026 17:29:07 +0530 Subject: [PATCH 35/40] upgrade db version --- .../51.json | 1002 +++++++++++++++++ 1 file changed, 1002 insertions(+) create mode 100644 app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/51.json diff --git a/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/51.json b/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/51.json new file mode 100644 index 000000000..68c461dfc --- /dev/null +++ b/app/schemas/com.pranshulgg.weather_master_app.data.local.WeatherMasterDatabase/51.json @@ -0,0 +1,1002 @@ +{ + "formatVersion": 1, + "database": { + "version": 51, + "identityHash": "9a80a1351686782b46b268933189e2b3", + "entities": [ + { + "tableName": "weather_locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `country` TEXT NOT NULL, `lat` REAL NOT NULL, `lon` REAL NOT NULL, `timezone` TEXT NOT NULL, `source` TEXT NOT NULL, `state` TEXT, `isFavorite` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL, `countryCode` TEXT, `isDefault` INTEGER NOT NULL, `isDeviceLocation` INTEGER NOT NULL, `alertSource` TEXT NOT NULL, `airQualitySource` TEXT NOT NULL, `customName` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "country", + "columnName": "country", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lat", + "columnName": "lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lon", + "columnName": "lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "timezone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT" + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPinned", + "columnName": "isPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countryCode", + "columnName": "countryCode", + "affinity": "TEXT" + }, + { + "fieldPath": "isDefault", + "columnName": "isDefault", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeviceLocation", + "columnName": "isDeviceLocation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alertSource", + "columnName": "alertSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "airQualitySource", + "columnName": "airQualitySource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "customName", + "columnName": "customName", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "weather_current", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperature` REAL, `humidity` REAL NOT NULL, `windSpeed` REAL, `windDirection` TEXT, `pressureMsl` REAL, `visibility` INTEGER, `cloudCover` REAL, `uvIndex` REAL, `weatherCondition` TEXT NOT NULL, `feelsLike` REAL, `time` INTEGER NOT NULL, `dewPoint` REAL, `utcOffsetSeconds` INTEGER, `lastUpdatedInMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudCover", + "columnName": "cloudCover", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndex", + "columnName": "uvIndex", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feelsLike", + "columnName": "feelsLike", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "utcOffsetSeconds", + "columnName": "utcOffsetSeconds", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdatedInMilli", + "columnName": "lastUpdatedInMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_weather_current_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_current_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_hourly", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperature` REAL, `windSpeed` REAL, `windDirection` TEXT, `rain` REAL NOT NULL, `snowfall` REAL, `uvIndex` REAL, `weatherCondition` TEXT NOT NULL, `pressureMsl` REAL, `visibility` INTEGER, `humidity` REAL, `dewPoint` REAL, `time` INTEGER NOT NULL, `precipitationProbability` INTEGER, PRIMARY KEY(`locationId`, `time`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL" + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snowfall", + "columnName": "snowfall", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndex", + "columnName": "uvIndex", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL" + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "precipitationProbability", + "columnName": "precipitationProbability", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId", + "time" + ] + }, + "indices": [ + { + "name": "index_weather_hourly_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_hourly_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_daily", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `temperatureMin` REAL, `temperatureMax` REAL, `windSpeed` REAL, `windDirection` TEXT, `rainSum` REAL NOT NULL, `snowfallSum` REAL, `uvIndexMax` REAL, `weatherCondition` TEXT NOT NULL, `time` INTEGER NOT NULL, `precipitationProbabilityMax` INTEGER, `pressureMsl` REAL, `visibility` INTEGER, `humidity` REAL, `dewPoint` REAL, `sunrise` INTEGER NOT NULL, `sunset` INTEGER NOT NULL, `moonrise` INTEGER NOT NULL, `moonset` INTEGER NOT NULL, `moonPhase` TEXT NOT NULL, `dawn` INTEGER NOT NULL, `dusk` INTEGER NOT NULL, PRIMARY KEY(`locationId`, `time`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "temperatureMin", + "columnName": "temperatureMin", + "affinity": "REAL" + }, + { + "fieldPath": "temperatureMax", + "columnName": "temperatureMax", + "affinity": "REAL" + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL" + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "TEXT" + }, + { + "fieldPath": "rainSum", + "columnName": "rainSum", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snowfallSum", + "columnName": "snowfallSum", + "affinity": "REAL" + }, + { + "fieldPath": "uvIndexMax", + "columnName": "uvIndexMax", + "affinity": "REAL" + }, + { + "fieldPath": "weatherCondition", + "columnName": "weatherCondition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "precipitationProbabilityMax", + "columnName": "precipitationProbabilityMax", + "affinity": "INTEGER" + }, + { + "fieldPath": "pressureMsl", + "columnName": "pressureMsl", + "affinity": "REAL" + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER" + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL" + }, + { + "fieldPath": "dewPoint", + "columnName": "dewPoint", + "affinity": "REAL" + }, + { + "fieldPath": "sunrise", + "columnName": "sunrise", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sunset", + "columnName": "sunset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonrise", + "columnName": "moonrise", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonset", + "columnName": "moonset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moonPhase", + "columnName": "moonPhase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dawn", + "columnName": "dawn", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dusk", + "columnName": "dusk", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId", + "time" + ] + }, + "indices": [ + { + "name": "index_weather_daily_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_weather_daily_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "weather_units", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tempUnit` TEXT NOT NULL, `windUnit` TEXT NOT NULL, `distanceUnit` TEXT NOT NULL, `pressureUnit` TEXT NOT NULL, `precipitationUnit` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tempUnit", + "columnName": "tempUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "windUnit", + "columnName": "windUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "distanceUnit", + "columnName": "distanceUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pressureUnit", + "columnName": "pressureUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precipitationUnit", + "columnName": "precipitationUnit", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "weather_blocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isDaily` INTEGER NOT NULL, `type` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDaily", + "columnName": "isDaily", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "air_quality_current", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `usAqi` INTEGER, `pm10` REAL, `pm25` REAL, `carbonMonoxide` REAL, `nitrogenDioxide` REAL, `sulphurDioxide` REAL, `ozone` REAL, `lastUpdatedInMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usAqi", + "columnName": "usAqi", + "affinity": "INTEGER" + }, + { + "fieldPath": "pm10", + "columnName": "pm10", + "affinity": "REAL" + }, + { + "fieldPath": "pm25", + "columnName": "pm25", + "affinity": "REAL" + }, + { + "fieldPath": "carbonMonoxide", + "columnName": "carbonMonoxide", + "affinity": "REAL" + }, + { + "fieldPath": "nitrogenDioxide", + "columnName": "nitrogenDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "sulphurDioxide", + "columnName": "sulphurDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "ozone", + "columnName": "ozone", + "affinity": "REAL" + }, + { + "fieldPath": "lastUpdatedInMilli", + "columnName": "lastUpdatedInMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_air_quality_current_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_air_quality_current_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nws_location_gridpoints", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `officeId` TEXT NOT NULL, `gridX` INTEGER NOT NULL, `gridY` INTEGER NOT NULL, `stationIdentifier` TEXT NOT NULL, `lastUpdatedMilli` INTEGER NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "officeId", + "columnName": "officeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gridX", + "columnName": "gridX", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gridY", + "columnName": "gridY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stationIdentifier", + "columnName": "stationIdentifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdatedMilli", + "columnName": "lastUpdatedMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_nws_location_gridpoints_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nws_location_gridpoints_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "github", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `currentTag` TEXT NOT NULL, `lastFetchedTag` TEXT NOT NULL, `lastFetchedTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentTag", + "columnName": "currentTag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchedTag", + "columnName": "lastFetchedTag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchedTime", + "columnName": "lastFetchedTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "air_quality_hourly", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `locationId` TEXT NOT NULL, `pm10` REAL, `pm25` REAL, `carbonMonoxide` REAL, `nitrogenDioxide` REAL, `sulphurDioxide` REAL, `ozone` REAL, `time` INTEGER NOT NULL, FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pm10", + "columnName": "pm10", + "affinity": "REAL" + }, + { + "fieldPath": "pm25", + "columnName": "pm25", + "affinity": "REAL" + }, + { + "fieldPath": "carbonMonoxide", + "columnName": "carbonMonoxide", + "affinity": "REAL" + }, + { + "fieldPath": "nitrogenDioxide", + "columnName": "nitrogenDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "sulphurDioxide", + "columnName": "sulphurDioxide", + "affinity": "REAL" + }, + { + "fieldPath": "ozone", + "columnName": "ozone", + "affinity": "REAL" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_air_quality_hourly_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_air_quality_hourly_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "accu_locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` TEXT NOT NULL, `cityKey` TEXT NOT NULL, PRIMARY KEY(`locationId`), FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cityKey", + "columnName": "cityKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_accu_locations_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_accu_locations_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "alerts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `locationId` TEXT NOT NULL, `event` TEXT NOT NULL, `severity` TEXT, `effective` INTEGER, `expires` INTEGER, `description` TEXT NOT NULL, `source` TEXT, `lastUpdatedInMilli` INTEGER NOT NULL, FOREIGN KEY(`locationId`) REFERENCES `weather_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "event", + "columnName": "event", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "severity", + "columnName": "severity", + "affinity": "TEXT" + }, + { + "fieldPath": "effective", + "columnName": "effective", + "affinity": "INTEGER" + }, + { + "fieldPath": "expires", + "columnName": "expires", + "affinity": "INTEGER" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdatedInMilli", + "columnName": "lastUpdatedInMilli", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_alerts_locationId", + "unique": false, + "columnNames": [ + "locationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_alerts_locationId` ON `${TABLE_NAME}` (`locationId`)" + } + ], + "foreignKeys": [ + { + "table": "weather_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "locationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a80a1351686782b46b268933189e2b3')" + ] + } +} \ No newline at end of file From f27d92cd793f28b67a967335d11870086cb195cf Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Thu, 30 Jul 2026 16:06:09 +0530 Subject: [PATCH 36/40] include next day hours closes #974 --- .../core/utils/weather/forecast/ForecastMatching.kt | 10 ++++++++-- .../feature/shared/ui/HourlyCard.kt | 11 ++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/forecast/ForecastMatching.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/forecast/ForecastMatching.kt index 366bee0d5..d44b355d8 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/forecast/ForecastMatching.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/utils/weather/forecast/ForecastMatching.kt @@ -34,7 +34,8 @@ fun findMatchingHourly( data: List, currentMilli: Long, source: WeatherSource, - zoneId: String + zoneId: String, + alwaysReturn24Hrs: Boolean = false ): List { @@ -44,7 +45,12 @@ fun findMatchingHourly( return emptyList() } val startDay = data[startIndex].time - return data.drop(maxOf(0, startIndex)).takeWhile { isSameDay(it.time, startDay, zoneId) } + + return if (alwaysReturn24Hrs) { + data.drop(maxOf(0, startIndex)).take(24) + } else { + data.drop(maxOf(0, startIndex)).takeWhile { isSameDay(it.time, startDay, zoneId) } + } } diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/ui/HourlyCard.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/ui/HourlyCard.kt index 025da215d..b625a7850 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/ui/HourlyCard.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/shared/ui/HourlyCard.kt @@ -41,6 +41,7 @@ import com.pranshulgg.weather_master_app.core.utils.formatters.to24HourTimeStrin import com.pranshulgg.weather_master_app.core.utils.weather.cache.isWeatherHourlyDomainSafe import com.pranshulgg.weather_master_app.core.utils.weather.forecast.findMatchingDaily import com.pranshulgg.weather_master_app.core.utils.weather.forecast.findMatchingHourly +import com.pranshulgg.weather_master_app.core.utils.weather.forecast.isSameDay import com.pranshulgg.weather_master_app.feature.blocks.components.NoHourlyDataAvailable import com.pranshulgg.weather_master_app.feature.shared.components.CardsHeader import java.time.ZoneId @@ -64,7 +65,8 @@ fun HourlyCard( weather.hourly, currentMilli, weather.location.source, - weather.location.timezone + weather.location.timezone, + alwaysReturn24Hrs = true ) @@ -123,7 +125,8 @@ fun HourlyCard( precipitationProbability = item.precipitationProbability ?: 0, temperature = temperature, isNow = index == 0, - icon = item.weatherCondition.toIcon(matchingDaily, item.time) + icon = item.weatherCondition.toIcon(matchingDaily, item.time), + isSameDay = isSameDay(item.time, currentMilli, weather.location.timezone) ) if (index == filteredHourly.size - 1) Gap(horizontal = 10.dp) @@ -141,12 +144,14 @@ private fun HourlyItem( precipitationProbability: Int, temperature: Double?, isNow: Boolean, - icon: Int + icon: Int, + isSameDay: Boolean = true ) { Column( modifier = Modifier .height(120.dp) + .alpha(if (!isSameDay) 0.6f else 1f) .padding(horizontal = 4.dp), verticalArrangement = Arrangement.Bottom, horizontalAlignment = Alignment.CenterHorizontally From 6af7d57ad35880b51d3503cefc153d2482bb0768 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Thu, 30 Jul 2026 16:06:34 +0530 Subject: [PATCH 37/40] edit location options --- .../editlocation/EditLocationScreen.kt | 105 ++++++++++++++---- .../editlocation/EditLocationViewModel.kt | 14 +++ .../ui/EditLocationScreenDialogs.kt | 25 +++++ .../feature/locations/LocationsScreen.kt | 7 +- .../components/LocationScreenSheetContent.kt | 10 +- .../locations/ui/LocationScreenSheet.kt | 7 +- 6 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/ui/EditLocationScreenDialogs.kt diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt index 6a75cf20b..6b7d19440 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -42,7 +43,9 @@ import com.pranshulgg.weather_master_app.core.ui.components.NavigateUpBtn import com.pranshulgg.weather_master_app.core.ui.components.SettingSection import com.pranshulgg.weather_master_app.core.ui.components.SettingTile import com.pranshulgg.weather_master_app.core.ui.components.Symbol +import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager import com.pranshulgg.weather_master_app.feature.editlocation.ui.EditLocationBottomSheet +import com.pranshulgg.weather_master_app.feature.editlocation.ui.EditLocationScreenDialogs import com.pranshulgg.weather_master_app.feature.shared.WeatherViewModel import com.pranshulgg.weather_master_app.feature.shared.ui.SharedBottomSheet @@ -55,7 +58,8 @@ data class EditLocationScreenUiState( val selectedAirQualitySource: AirQualitySource? = null, val isAlertSourcesSheetOpen: Boolean = false, val isAirQualitySourcesSheetOpen: Boolean = false, - val isEditLocationNameSheetOpen: Boolean = false + val isEditLocationNameSheetOpen: Boolean = false, + val isConfirmationDialogOpen: Boolean = false ) @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @@ -74,7 +78,6 @@ fun EditLocationScreen( if (uiState.location == null) return - val btnSize = ButtonDefaults.MediumContainerHeight val sheetState = rememberBottomSheetState( initialValue = SheetValue.Hidden, @@ -173,11 +176,7 @@ fun EditLocationScreen( modifier = Modifier.padding(top = 10.dp, start = 16.dp, end = 16.dp) ) Gap(26.dp) - Button( - modifier = Modifier - .heightIn(btnSize) - .fillMaxWidth() - .padding(horizontal = 16.dp), + ButtonWithIcon( onClick = { viewModel.saveLocationName( if (currentLocationName.trim() == locationText.trim()) null else currentLocationName.trim(), @@ -192,21 +191,32 @@ fun EditLocationScreen( uiState.selectedAlertSource ?: uiState.location.alertSource ) }, - shapes = ButtonDefaults.shapes(), - contentPadding = ButtonDefaults.contentPaddingFor(btnSize), - ) { - Symbol( - R.drawable.check_24px, - color = MaterialTheme.colorScheme.onPrimary, - size = ButtonDefaults.iconSizeFor(btnSize) - ) - Gap(horizontal = ButtonDefaults.iconSpacingFor(btnSize)) - Text( - stringResource(R.string.action_save_changes), - style = ButtonDefaults.textStyleFor(btnSize), - color = MaterialTheme.colorScheme.onPrimary - ) - } + text = stringResource(R.string.action_save_changes), + icon = R.drawable.check_24px + ) + Gap(8.dp) + ButtonWithIcon( + onClick = { + viewModel.updateDefaultLocation(uiState.location.id) + navController.popBackStack() + }, + text = stringResource(R.string.action_set_default), + icon = R.drawable.home_pin_24px, + ) + Gap(8.dp) + ButtonWithIcon( + onClick = { + if (uiState.location.isDefault) { + SnackbarManager.show(R.string.error_delete_default_location) + return@ButtonWithIcon + } + viewModel.showConfirmationDialog() + }, + text = stringResource(R.string.action_delete), + icon = R.drawable.delete_24px, + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) // WEATHER SOURCES SHEET SharedBottomSheet.WeatherSourcesForLocationSheet( @@ -260,9 +270,58 @@ fun EditLocationScreen( value = currentLocationName, ) - Gap(WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()) + // CONFIRMATION DIALOG + EditLocationScreenDialogs.EditLocationScreenConfirmationDialog( + viewModel, + onConfirm = { + weatherViewModel.deleteLocation(uiState.location.id) + viewModel.hideConfirmationDialog() + navController.popBackStack() + } + ) } + + Gap(WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()) } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun ButtonWithIcon( + onClick: () -> Unit, + text: String, + icon: Int, + containerColor: Color = MaterialTheme.colorScheme.primary, + contentColor: Color = MaterialTheme.colorScheme.onPrimary, + enabled: Boolean = true +) { + + val btnSize = ButtonDefaults.MediumContainerHeight + + Button( + colors = ButtonDefaults.buttonColors( + containerColor = containerColor + ), + modifier = Modifier + .heightIn(btnSize) + .fillMaxWidth() + .padding(horizontal = 16.dp), + onClick = onClick, + enabled = enabled, + shapes = ButtonDefaults.shapes(), + contentPadding = ButtonDefaults.contentPaddingFor(btnSize), + ) { + Symbol( + icon, + color = contentColor, + size = ButtonDefaults.iconSizeFor(btnSize) + ) + Gap(horizontal = ButtonDefaults.iconSpacingFor(btnSize)) + Text( + text, + style = ButtonDefaults.textStyleFor(btnSize), + color = contentColor + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationViewModel.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationViewModel.kt index 0922f9307..dab3acbf4 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationViewModel.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/EditLocationViewModel.kt @@ -80,4 +80,18 @@ class EditLocationViewModel @Inject constructor( fun hideEditLocationNameSheet() { _uiState.value = _uiState.value.copy(isEditLocationNameSheetOpen = false) } + + fun showConfirmationDialog() { + _uiState.value = _uiState.value.copy(isConfirmationDialogOpen = true) + } + + fun hideConfirmationDialog() { + _uiState.value = _uiState.value.copy(isConfirmationDialogOpen = false) + } + + fun updateDefaultLocation(id: String) { + viewModelScope.launch { + locationsRepo.updateDefaultLocation(id) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/ui/EditLocationScreenDialogs.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/ui/EditLocationScreenDialogs.kt new file mode 100644 index 000000000..58df7f4a9 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/editlocation/ui/EditLocationScreenDialogs.kt @@ -0,0 +1,25 @@ +package com.pranshulgg.weather_master_app.feature.editlocation.ui + +import androidx.compose.runtime.Composable +import com.pranshulgg.weather_master_app.core.ui.components.TextAlertDialog +import com.pranshulgg.weather_master_app.feature.editlocation.EditLocationViewModel +import com.pranshulgg.weather_master_app.feature.locations.LocationsScreenViewModel +import com.pranshulgg.weather_master_app.feature.shared.WeatherViewModel + +object EditLocationScreenDialogs { + @Composable + fun EditLocationScreenConfirmationDialog( + viewModel: EditLocationViewModel, + onConfirm: () -> Unit + ) { + val uiState = viewModel.uiState.value + + TextAlertDialog( + show = uiState.isConfirmationDialogOpen, + onConfirm = onConfirm, + onDismiss = viewModel::hideConfirmationDialog, + title = "Delete Location", + message = "Are you sure you want to delete this location?" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt index 790f71640..513ce3ec0 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt @@ -64,8 +64,7 @@ fun LocationsScreen( onLocationSelect: (Location) -> Unit, isTabletLike: Boolean = false, weatherViewModel: WeatherViewModel, - - ) { +) { val viewModel: LocationsScreenViewModel = hiltViewModel() val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -139,7 +138,9 @@ fun LocationsScreen( LocationScreenConfirmationDialog(weatherViewModel, viewModel) - LocationScreenSheet(viewModel, sheetState) + LocationScreenSheet(viewModel, sheetState, onEdit = { + navController.navigate(NavRoutes.editLocation(uiState.value.longClickedLocation!!.id)) + }) SharedDialogs.DeviceBackgroundLocationPermissionInfoDialog( show = backgroundLocationPermissionInfoDialogOpen, diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/components/LocationScreenSheetContent.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/components/LocationScreenSheetContent.kt index b6c88b1bb..006776e66 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/components/LocationScreenSheetContent.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/components/LocationScreenSheetContent.kt @@ -11,12 +11,20 @@ import com.pranshulgg.weather_master_app.core.ui.components.SettingsTileIcon fun LocationScreenSheetContent( locationName: String, onDelete: () -> Unit, - onSetAsDefault: () -> Unit + onSetAsDefault: () -> Unit, + onEdit: () -> Unit ) { SettingSection( isModalOption = true, title = locationName, tiles = listOf( + SettingTile.ActionTile( + leading = { SettingsTileIcon(R.drawable.edit_24px) }, + title = stringResource(R.string.location_edit), + onClick = { + onEdit() + } + ), SettingTile.ActionTile( leading = { SettingsTileIcon(R.drawable.delete_24px) }, title = stringResource(R.string.action_delete), diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/ui/LocationScreenSheet.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/ui/LocationScreenSheet.kt index eb5d6b213..235f22843 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/ui/LocationScreenSheet.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/ui/LocationScreenSheet.kt @@ -13,7 +13,8 @@ import com.pranshulgg.weather_master_app.feature.locations.components.LocationSc @Composable fun LocationScreenSheet( viewModel: LocationsScreenViewModel, - sheetState: SheetState + sheetState: SheetState, + onEdit: () -> Unit ) { val uiState = viewModel.uiState.value @@ -42,6 +43,10 @@ fun LocationScreenSheet( if (uiState.longClickedLocation != null && !uiState.longClickedLocation.isDefault) { viewModel.updateDefaultLocation(uiState.longClickedLocation.id) } + }, + onEdit = { + hide() + onEdit() } ) From 6c6600d02c0f011ddf235c2e3d04dad0e41bacf9 Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Thu, 30 Jul 2026 16:06:44 +0530 Subject: [PATCH 38/40] fix credits --- .../feature/main/ui/MainScreenBottomSheets.kt | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/MainScreenBottomSheets.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/MainScreenBottomSheets.kt index 9bc4d8894..ff6d000c9 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/MainScreenBottomSheets.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/MainScreenBottomSheets.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.pranshulgg.weather_master_app.R import com.pranshulgg.weather_master_app.core.model.domain.location.Location +import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource +import com.pranshulgg.weather_master_app.core.model.sources.AlertSource import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource import com.pranshulgg.weather_master_app.core.ui.components.ActionBottomSheet import com.pranshulgg.weather_master_app.core.ui.components.DialogBasic @@ -64,20 +66,38 @@ object MainScreenBottomSheets { ) ) ) - Gap(8.dp) - SettingSection( - title = stringResource(R.string.weather_air_quality), - tiles = listOf( - SettingTile.ActionTile( - title = location.airQualitySource.fullName, - description = location.airQualitySource.displayLink, - trailing = { SettingsTileIcon(R.drawable.open_in_new_24px) }, - onClick = { - uriHandler.openUri(location.airQualitySource.displayLink) - } + if (location.airQualitySource != AirQualitySource.NONE) { + Gap(8.dp) + SettingSection( + title = stringResource(R.string.weather_air_quality), + tiles = listOf( + SettingTile.ActionTile( + title = location.airQualitySource.fullName, + description = location.airQualitySource.displayLink, + trailing = { SettingsTileIcon(R.drawable.open_in_new_24px) }, + onClick = { + uriHandler.openUri(location.airQualitySource.displayLink) + } + ) ) ) - ) + } + if (location.alertSource != AlertSource.NONE) { + Gap(8.dp) + SettingSection( + title = "Alerts", + tiles = listOf( + SettingTile.ActionTile( + title = location.alertSource.fullName, + description = location.alertSource.displayLink, + trailing = { SettingsTileIcon(R.drawable.open_in_new_24px) }, + onClick = { + uriHandler.openUri(location.alertSource.displayLink) + } + ) + ) + ) + } } } } From 2d575b8167199ac3e3a45c8967ebccb602b82bcd Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Thu, 30 Jul 2026 16:07:10 +0530 Subject: [PATCH 39/40] new strings --- app/src/main/res/values/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e441cc845..992e37242 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -389,6 +389,11 @@ Low Unknown + Delete Location + Are you sure you want to delete this location? + + + Alerts From bff0c7465d7e960085d4c0038af927f270010ccd Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Thu, 30 Jul 2026 16:07:24 +0530 Subject: [PATCH 40/40] new version check every 2hours --- .../weather_master_app/core/network/github/GithubRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/github/GithubRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/github/GithubRepository.kt index ac92cce11..f0dbfe212 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/github/GithubRepository.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/github/GithubRepository.kt @@ -17,7 +17,7 @@ class GithubRepository @Inject constructor( if (getSavedVersionInfo != null && getSavedVersionInfo.currentTag == currentTag) { val ageMillis = System.currentTimeMillis() - getSavedVersionInfo.lastFetchedTime - val maxAgeMillis = TimeUnit.HOURS.toMillis(24) + val maxAgeMillis = TimeUnit.HOURS.toMillis(2) if (ageMillis < maxAgeMillis) { return@withContext getSavedVersionInfo.lastFetchedTag != currentTag