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 { 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 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/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 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..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 @@ -23,6 +23,8 @@ 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.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 @@ -79,8 +81,12 @@ object AppModule { @Singleton fun provideWeatherDataReconcilerRepository( nwsDao: NwsDao, - locationsDao: LocationsDao - ): WeatherDataReconcilerRepository = WeatherDataReconcilerRepository(nwsDao, locationsDao) + locationsDao: LocationsDao, + accuDao: AccuDao, + airQualityDao: AirQualityDao, + alertsDao: AlertsDao + ): WeatherDataReconcilerRepository = + WeatherDataReconcilerRepository(nwsDao, locationsDao, accuDao, airQualityDao, alertsDao) @Provides @Singleton @@ -93,5 +99,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..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 @@ -34,4 +34,10 @@ object DaoModule { @Provides fun provideGithubDao(db: WeatherMasterDatabase) = db.githubDao() + + @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 0012864e4..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 @@ -2,7 +2,10 @@ 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.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 @@ -99,4 +102,16 @@ object ApiModule { @Provides @Singleton fun provideMeteoamApi(): MeteoamApi = MeteoamApi.create() + + @Provides + @Singleton + fun provideAccuAqiApi(): AccuAqiApi = AccuAqiApi.create() + + @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 0abb1ad24..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 @@ -1,7 +1,13 @@ 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.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 @@ -27,6 +33,8 @@ 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.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 @@ -133,8 +141,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 +152,30 @@ 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) + + @Provides + @Singleton + fun provideAccuAlertsRepository( + api: AlertsAccuApi, + dao: AlertsDao, + accuDao: AccuDao, + 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 new file mode 100644 index 000000000..c036770db --- /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/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/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..dde7aa0ff --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/weather/alerts/AlertSeverity.kt @@ -0,0 +1,36 @@ +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, + val contentColor: Color, +) { + CRITICAL( + R.string.alert_severity_critical, + Color(0xFFD32F2F), + Color(0xFFFFEBEE) + ), + HIGH( + R.string.alert_severity_high, + Color(0xFFF57C00), + Color(0xFFFFF4E0) + ), + MODERATE( + R.string.alert_severity_medium, + Color(0xFFFBC02D), + Color(0xFF5C4300) + ), + LOW( + R.string.alert_severity_low, + Color(0xFF388E3C), + Color(0xFF0F3D0F) + ), + UNKNOWN( + R.string.alert_severity_unknown, + Color(0xFF757575), + Color(0xFFF3E5F5) + ) +} \ No newline at end of file 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 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/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/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/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/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/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/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 ) } 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..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 @@ -13,14 +13,17 @@ 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 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 @@ -45,6 +48,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 @@ -67,13 +71,18 @@ fun AppNavHost( ) { navigation( route = "root", -// startDestination = NavRoutes.MAIN - startDestination = NavRoutes.editLocation("af673c89-72b7-4da9-865d-c26fdabeca07") + startDestination = NavRoutes.MAIN +// startDestination = NavRoutes.alerts("5e8c6c88-e85a-4e76-8d0b-5d1eb2906acd") ) { composable( NavRoutes.MAIN - ) { - MainScreen(navController) + ) { backStackEntry -> + + val rootEntry = remember(backStackEntry) { + navController.getBackStackEntry("root") + } + val weatherViewModel: WeatherViewModel = hiltViewModel(rootEntry) + MainScreen(navController, weatherViewModel) } composable( NavRoutes.SEARCH @@ -122,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 ) { @@ -164,7 +185,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}", 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" } 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/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/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) 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/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/data/local/WeatherMasterDatabase.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/WeatherMasterDatabase.kt index 738c6dbea..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 @@ -8,6 +8,8 @@ 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.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 @@ -17,6 +19,8 @@ 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.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 @@ -36,15 +40,20 @@ import com.pranshulgg.weather_master_app.data.local.entity.weather.nws.NwsGridPo CurrentAirQualityEntity::class, NwsGridPointsEntity::class, GithubEntity::class, - HourlyAirQualityEntity::class + HourlyAirQualityEntity::class, + AccuEntity::class, + AlertEntity::class ], - version = 48, + version = 51, 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), + AutoMigration(from = 49, to = 50), + AutoMigration(from = 50, to = 51) ] ) abstract class WeatherMasterDatabase : RoomDatabase() { @@ -57,6 +66,10 @@ abstract class WeatherMasterDatabase : RoomDatabase() { abstract fun nwsDao(): NwsDao abstract fun githubDao(): GithubDao + 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/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/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..ff22fdcf3 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/dao/alerts/AlertsDao.kt @@ -0,0 +1,35 @@ +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 +import kotlinx.coroutines.flow.Flow + + +@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) + + @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 87cbae39a..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 @@ -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 @@ -79,4 +81,12 @@ 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?) + } \ 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/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..d0bbfe988 --- /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/airquality/AqiDomainMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/AqiDomainMapper.kt new file mode 100644 index 000000000..acd6c7b43 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/AqiDomainMapper.kt @@ -0,0 +1,32 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.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.data.local.entity.airquality.CurrentAirQualityEntity +import com.pranshulgg.weather_master_app.data.local.entity.airquality.HourlyAirQualityEntity + + +fun AirQualityCurrent.toEntity(locationId: String): CurrentAirQualityEntity = + CurrentAirQualityEntity( + locationId = locationId, + usAqi = usAqi, + pm10 = pm10, + pm25 = pm25, + carbonMonoxide = carbonMonoxide, + nitrogenDioxide = nitrogenDioxide, + sulphurDioxide = sulphurDioxide, + ozone = ozone, + lastUpdatedInMilli = lastUpdatedInMilli + ) + +fun AirQualityHourly.toEntity(locationId: String): HourlyAirQualityEntity = + HourlyAirQualityEntity( + locationId = locationId, + pm10 = pm10, + pm25 = pm25, + carbonMonoxide = carbonMonoxide, + nitrogenDioxide = nitrogenDioxide, + sulphurDioxide = sulphurDioxide, + ozone = ozone, + time = time + ) \ 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/airquality/OpenMeteoAqiMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/airquality/sources/openmeteo/OpenMeteoAqiMapper.kt similarity index 58% 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/sources/openmeteo/OpenMeteoAqiMapper.kt index bb04f9207..f9da78116 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/sources/openmeteo/OpenMeteoAqiMapper.kt @@ -1,11 +1,10 @@ -package com.pranshulgg.weather_master_app.data.local.mapper.airquality +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 -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( @@ -30,30 +29,4 @@ fun OpenMeteoAqiDto.toDomain(): AirQuality = ozone = hourly.ozone[it] ) } - ) - - -fun AirQualityCurrent.toEntity(locationId: String): CurrentAirQualityEntity = - CurrentAirQualityEntity( - locationId = locationId, - usAqi = usAqi, - pm10 = pm10, - pm25 = pm25, - carbonMonoxide = carbonMonoxide, - nitrogenDioxide = nitrogenDioxide, - sulphurDioxide = sulphurDioxide, - ozone = ozone, - lastUpdatedInMilli = lastUpdatedInMilli - ) - -fun AirQualityHourly.toEntity(locationId: String): HourlyAirQualityEntity = - HourlyAirQualityEntity( - locationId = locationId, - pm10 = pm10, - pm25 = pm25, - carbonMonoxide = carbonMonoxide, - nitrogenDioxide = nitrogenDioxide, - sulphurDioxide = sulphurDioxide, - ozone = ozone, - time = time ) \ No newline at end of file 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/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/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/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 } 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/provider/AlertsRepositoryProvider.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt new file mode 100644 index 000000000..59a657fda --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt @@ -0,0 +1,23 @@ +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 alertsWeatherApiRepository: AlertsWeatherApiRepository +) { + + fun getRepository(source: AlertSource): AlertRepository? { + return when (source) { + AlertSource.ACCU_WEATHER -> alertsAccuRepository + AlertSource.WEATHER_API -> alertsWeatherApiRepository + AlertSource.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 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/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/data/repository/LocationsRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/repository/LocationsRepository.kt index f28542cd0..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,18 +1,20 @@ 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 +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.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 @@ -23,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 @@ -79,10 +79,17 @@ 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() } + suspend fun updateLocationCustomName(id: String, name: String?) { + dao.updateLocationCustomName(id, name) + } @Transaction suspend fun saveLocation(location: Location) { @@ -204,6 +211,8 @@ class LocationsRepository @Inject constructor( return dao.getAllLocationsCurrentWeather() .map { list -> list.map { it.toDomain() } } } + + } 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..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 @@ -1,6 +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 @@ -8,7 +13,10 @@ 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, + private val alertsDao: AlertsDao ) { /** @@ -17,14 +25,56 @@ 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, + currentAlertSource: AlertSource + ) { when (previousSource) { - WeatherSource.NWS -> cleanNws(locationId) + WeatherSource.NWS -> nwsDao.deleteGridPointsForLocation(locationId) + WeatherSource.ACCU_WEATHER -> cleanAccuWeather( + locationId, + airQualitySource, + currentAlertSource + ) + else -> {} } } - suspend fun cleanNws(locationId: String) { - nwsDao.deleteGridPointsForLocation(locationId) + suspend fun cleanAccuWeather( + locationId: String, airQualitySource: AirQualitySource, + currentAlertSource: AlertSource + ) { + if (airQualitySource != AirQualitySource.ACCU_WEATHER || currentAlertSource != AlertSource.ACCU_WEATHER) { + accuDao.deleteCityKeyForLocation(locationId) + } } + + suspend fun cleanUpStaleAirQualityData( + locationId: String, + currentWeatherSource: WeatherSource, + currentAlertSource: AlertSource + ) { + airQualityDao.deleteCurrentAirQuality(locationId) + airQualityDao.deleteHourlyAirQuality(locationId) + + if (currentWeatherSource != WeatherSource.ACCU_WEATHER || currentAlertSource != AlertSource.ACCU_WEATHER) { + accuDao.deleteCityKeyForLocation(locationId) + } + } + + suspend fun cleanUpStaleAlertsData( + locationId: String, + currentWeatherSource: WeatherSource, + airQualitySource: AirQualitySource, + ) { + alertsDao.deleteAlertsForLocation(locationId) + + if (currentWeatherSource != WeatherSource.ACCU_WEATHER || airQualitySource != AirQualitySource.ACCU_WEATHER) { + accuDao.deleteCityKeyForLocation(locationId) + } + } + } \ 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/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 +} 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..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 @@ -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 @@ -29,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 @@ -38,15 +37,16 @@ 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 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 @@ -58,13 +58,15 @@ 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) @Composable fun EditLocationScreen( - navController: NavController, id: String + navController: NavController, id: String, + weatherViewModel: WeatherViewModel ) { val viewModel: EditLocationViewModel = hiltViewModel() @@ -76,7 +78,6 @@ fun EditLocationScreen( if (uiState.location == null) return - val btnSize = ButtonDefaults.MediumContainerHeight val sheetState = rememberBottomSheetState( initialValue = SheetValue.Hidden, @@ -125,11 +126,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") } ) ) ) @@ -174,27 +176,47 @@ 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), - onClick = { /* Handle click */ }, - shapes = ButtonDefaults.shapes(), - contentPadding = ButtonDefaults.contentPaddingFor(btnSize), - ) { - Symbol( - R.drawable.check_24px, - color = MaterialTheme.colorScheme.onPrimaryContainer, - size = ButtonDefaults.iconSizeFor(btnSize) - ) - Gap(horizontal = ButtonDefaults.iconSpacingFor(btnSize)) - Text( - stringResource(R.string.action_save_changes), - style = ButtonDefaults.textStyleFor(btnSize), - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } + ButtonWithIcon( + 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, + uiState.selectedAlertSource ?: uiState.location.alertSource + ) + }, + 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( @@ -248,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 1d743d48b..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 @@ -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) } @@ -74,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/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/LocationsScreen.kt b/app/src/main/java/com/pranshulgg/weather_master_app/feature/locations/LocationsScreen.kt index c8cce5509..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 @@ -62,11 +62,11 @@ 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 +74,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) } @@ -110,7 +112,7 @@ fun LocationsScreen( floatingActionButton = { FloatingButton(navController) }, - floatingActionButtonPosition = FabPosition.Center + floatingActionButtonPosition = FabPosition.Center, ) { paddingValues -> Box( modifier = Modifier @@ -128,14 +130,17 @@ fun LocationsScreen( activeLocation = activeLocation, weatherForLocations, onAddCurrentLocation = { locationPermissionInfoDialogOpen = true }, - uiState.value.isDeviceLocationLoading + uiState.value.isDeviceLocationLoading, + alertsForLocations ) } } LocationScreenConfirmationDialog(weatherViewModel, viewModel) - LocationScreenSheet(viewModel, sheetState) + LocationScreenSheet(viewModel, sheetState, onEdit = { + navController.navigate(NavRoutes.editLocation(uiState.value.longClickedLocation!!.id)) + }) SharedDialogs.DeviceBackgroundLocationPermissionInfoDialog( show = backgroundLocationPermissionInfoDialogOpen, @@ -185,6 +190,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) + } } ) } 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..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,6 +31,12 @@ class LocationsScreenViewModel @Inject constructor( initialValue = emptyList() ) + val allLocationsAlerts = alertsRepository.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/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() } ) 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..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,11 +24,13 @@ 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 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 @@ -50,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( @@ -63,8 +66,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 @@ -146,7 +148,8 @@ fun MainScreen(navController: NavController) { weatherViewModel.setActiveLocation(it) } }, - isTabletLike = isTabletLike + isTabletLike = isTabletLike, + weatherViewModel ) }, drawerState = drawerState, @@ -166,7 +169,7 @@ fun MainScreen(navController: NavController) { } }, onEditLocation = { - viewModel.showWeatherSourcesForLocationSheet(uiState.isLoading) + navController.navigate(NavRoutes.editLocation(activeLocation!!.id)) }, context, onWeatherSourceInfoClick = viewModel::showWeatherSourcesInfoForLocationSheet, @@ -176,21 +179,6 @@ 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 - ) - // 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..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 @@ -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 @@ -31,9 +32,11 @@ 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 +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 +63,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 +160,20 @@ fun MainScreenScaffold( ), verticalArrangement = Arrangement.spacedBy(14.dp) ) { + if (alerts.isNotEmpty()) { + AlertsSection( + alerts, + prefs, + weather.location.timezone, + onAlertClick = { + navController.navigate( + NavRoutes.alerts( + weather.location.id + ) + ) + } + ) + } if (isShowSummary) { SummaryCard( weather, 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), 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..4264874c7 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/feature/main/ui/AlertsSection.kt @@ -0,0 +1,80 @@ +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 +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, + onAlertClick: () -> Unit +) { + + 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 + ), + modifier = Modifier.clickable(onClick = onAlertClick), + leadingContent = { + Symbol( + R.drawable.warning_24px, + color = it.severity?.color ?: AlertSeverity.UNKNOWN.color, + size = 32.dp + ) + }, + headlineContent = { Text(it.event) }, + supportingContent = { + if (it.effective != null && it.expires != null) { + Text("${formatter(it.effective)} • ${formatter(it.expires)}") + } + } + ) + } + } + + } +} \ No newline at end of file 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..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 @@ -42,7 +44,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( @@ -65,20 +66,38 @@ object MainScreenBottomSheets { ) ) ) - Gap(8.dp) - SettingSection( - title = stringResource(R.string.weather_air_quality), - tiles = listOf( - SettingTile.ActionTile( - title = WeatherSource.OPEN_METEO.displayName, - description = airQualityUrl, - trailing = { SettingsTileIcon(R.drawable.open_in_new_24px) }, - onClick = { - uriHandler.openUri(airQualityUrl) - } + 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) + } + ) + ) + ) + } } } } 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() } 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") } +// ) ) ) 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..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 @@ -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 @@ -11,13 +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 @@ -36,6 +41,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 +51,8 @@ class WeatherViewModel @Inject constructor( private val weatherBlocksRepository: WeatherBlocksRepository, private val openMeteoAqiRepository: OpenMeteoAqiRepository, private val weatherDataReconcilerRepository: WeatherDataReconcilerRepository, + private val airQualityRepositoryProvider: AirQualityRepositoryProvider, + private val alertRepositoryProvider: AlertsRepositoryProvider, @ApplicationContext private val context: Context ) : ViewModel() { @@ -108,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() @@ -125,7 +134,10 @@ class WeatherViewModel @Inject constructor( // Run separately if (!_uiState.value.isError) { launch { - handleAirQuality(location, isManualRefresh) + handleAirQuality(location, isManualRefresh, isForceRefreshForAirQuality) + } + launch { + handleAlerts(location, isManualRefresh, isForceRefreshForAlerts) } } @@ -139,8 +151,10 @@ class WeatherViewModel @Inject constructor( delay(minLoadingTime - elapsed) } + setLoading(false) + } } @@ -166,50 +180,51 @@ 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, + 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) + weatherDataReconcilerRepository.cleanUpStaleData( + location.source, + location.id, + airQualitySource, + alertSource + ) + } + if (allowForceRefreshForAirQuality) { + weatherDataReconcilerRepository.cleanUpStaleAirQualityData( + location.id, + source, + alertSource + ) + } + if (allowForceRefreshForAlerts) { + weatherDataReconcilerRepository.cleanUpStaleAlertsData( + location.id, + source, + airQualitySource + ) } _uiState.value = _uiState.value.copy( activeLocation = updatedLocation @@ -218,7 +233,8 @@ class WeatherViewModel @Inject constructor( updatedLocation, source, isForceRefresh = allowForceRefreshForWeather, - isForceRefreshForAirQuality = allowForceRefreshForAirQuality + isForceRefreshForAirQuality = allowForceRefreshForAirQuality, + isForceRefreshForAlerts = allowForceRefreshForAlerts ) @@ -305,25 +321,79 @@ 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 + ) } } } + 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 + ) + } + } + } } 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) + } } ) } 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 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 @@ + + + diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index e6d0e72d0..3074b59fb 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -279,8 +279,8 @@ Von %1$s %1$s Std Keine Ergebnisse gefunden - Es wird den ganzen Tag und die ganze Nacht %1$s bleiben. - Am Tag wird es %1$s sein und in der Nacht %2$s werden. + 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. Heute Nacht besteht gegen %2$s eine Regenwahrscheinlichkeit von %1$s. diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 4130b92b2..228c61a96 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -284,11 +284,11 @@ 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. - - Se pronostica nieve alrededor del %1$s durante el día con una probabilidad del %2$s. - Durante la noche, hay un %1$s de probabilidad de nieve alrededor del %2$s. - Se espera calor extremo durante el día con una máxima de %1$s. Las temperaturas bajarán alrededor de %2$s. - Hará calor durante el día, con máximas cercanas a %1$s. Esta noche, las temperaturas bajarán alrededor de %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. + It will be warm during the day with highs near %1$s. Tonight, temperatures will fall to around %2$s. A mild day is expected with temperatures reaching %1$s. Overnight, it\'ll cool to around %2$s. Hará frío durante el día, con máximas cercanas a %1$s. Esta noche, las temperaturas bajarán alrededor de %2$s Cold weather continues with daytime temperatures around %1$s. Overnight, lows will fall to around %2$s. diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index bd400f26a..40e113622 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. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 099e8513d..1a4307d32 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. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d472f9672..992e37242 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -381,6 +381,20 @@ Air quality source Location name Location information + Long press a location for more options + + Critical + High + Medium + Low + Unknown + + Delete Location + Are you sure you want to delete this location? + + + Alerts +