mirror of
https://github.com/PranshulGG/WeatherMaster.git
synced 2026-10-22 02:52:37 -05:00
Merge branch 'main' into l10n_main
Signed-off-by: Pranshul <pranshul.devmain@gmail.com>
This commit is contained in:
88 files changed
+5014
-238
No files matched your search
@@ -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 {
|
||||
|
||||
+916
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
+1004
File diff suppressed because it is too large.
Load diff
+1002
File diff suppressed because it is too large.
Load diff
@@ -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)
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
+37
-2
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
+1
-1
@@ -16,6 +16,6 @@ enum class AirQualitySource(
|
||||
displayName = "Open Meteo",
|
||||
fullName = "Open Meteo",
|
||||
displayLink = "https://open-meteo.com/"
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.pranshulgg.weather_master_app.core.model.weather.airquality.accu
|
||||
|
||||
|
||||
data class AccuCityKey(
|
||||
val locationId: String,
|
||||
val cityKey: String
|
||||
)
|
||||
+15
@@ -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<Alert>) : AlertResult()
|
||||
data class Error(val exception: Exception, val cacheAlerts: List<Alert> = emptyList()) :
|
||||
AlertResult()
|
||||
}
|
||||
|
||||
enum class AlertResultType {
|
||||
RETURN_CACHE,
|
||||
ERROR
|
||||
}
|
||||
+36
@@ -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)
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
|
||||
+68
@@ -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<AccuAqiJson>
|
||||
|
||||
@GET("airquality/v2/forecasts/hourly/24hour/{locationKey}")
|
||||
suspend fun fetchForecast(
|
||||
@Path("locationKey") locationKey: String,
|
||||
@Query("pollutants") details: Boolean = true
|
||||
): Response<AccuAqiForecastJson>
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+95
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -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<AccuAqiPollutantJson>
|
||||
)
|
||||
|
||||
data class AccuAqiPollutantJson(
|
||||
val type: String,
|
||||
val concentration: AccuAqiConcentrationJson
|
||||
)
|
||||
|
||||
data class AccuAqiConcentrationJson(
|
||||
val value: Double?
|
||||
)
|
||||
|
||||
data class AccuAqiForecastJson(
|
||||
val data: List<AccuAqiForecastDataJson>
|
||||
)
|
||||
|
||||
data class AccuAqiForecastDataJson(
|
||||
@SerializedName("epochDate") val timeSeconds: Long,
|
||||
val pollutants: List<AccuAqiForecastPollutantJson>
|
||||
)
|
||||
|
||||
data class AccuAqiForecastPollutantJson(
|
||||
val type: String,
|
||||
val concentration: AccuAqiConcentrationJson
|
||||
)
|
||||
+9
@@ -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
|
||||
)
|
||||
+7
-4
@@ -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())
|
||||
|
||||
+59
@@ -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<List<AlertsAccuJson>>
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
@@ -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() })
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+24
@@ -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<AlertsAccuAreaJson>,
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
)
|
||||
+55
@@ -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<AlertsWeatherApiJson>
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -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() })
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+19
@@ -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<AlertsWeatherApiAlertItemJson>
|
||||
)
|
||||
|
||||
data class AlertsWeatherApiAlertItemJson(
|
||||
val event: String,
|
||||
val effective: String?,
|
||||
val expires: String?,
|
||||
val desc: String?,
|
||||
val severity: String?,
|
||||
val headline: String?
|
||||
)
|
||||
+17
-7
@@ -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),
|
||||
|
||||
+5
-5
@@ -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?
|
||||
)
|
||||
+3
-1
@@ -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(
|
||||
|
||||
+3
-1
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
+31
-5
@@ -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}",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
+6
-1
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+38
@@ -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<AlertEntity?>,
|
||||
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
|
||||
}
|
||||
+9
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -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)
|
||||
|
||||
+8
-2
@@ -34,7 +34,8 @@ fun findMatchingHourly(
|
||||
data: List<WeatherHourly>,
|
||||
currentMilli: Long,
|
||||
source: WeatherSource,
|
||||
zoneId: String
|
||||
zoneId: String,
|
||||
alwaysReturn24Hrs: Boolean = false
|
||||
): List<WeatherHourly> {
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+46
@@ -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
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -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
|
||||
|
||||
+2
-2
@@ -18,9 +18,9 @@ interface AirQualityDao {
|
||||
hourlyAirQuality: List<HourlyAirQualityEntity>,
|
||||
id: String
|
||||
) {
|
||||
insertCurrentAirQuality(currentAirQuality)
|
||||
|
||||
deleteCurrentAirQuality(id)
|
||||
deleteHourlyAirQuality(id)
|
||||
insertCurrentAirQuality(currentAirQuality)
|
||||
insertHourlyAirQuality(hourlyAirQuality)
|
||||
}
|
||||
|
||||
|
||||
+22
@@ -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)
|
||||
}
|
||||
+35
@@ -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<AlertEntity>, locationId: String) {
|
||||
deleteAlertsForLocation(locationId)
|
||||
insertAlertList(alerts)
|
||||
}
|
||||
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
|
||||
suspend fun insertAlertList(alerts: List<AlertEntity>)
|
||||
|
||||
@Query("SELECT * FROM alerts WHERE locationId = :locationId")
|
||||
suspend fun getAlertsForLocation(locationId: String): List<AlertEntity?>
|
||||
|
||||
@Query("DELETE FROM alerts WHERE locationId = :locationId")
|
||||
suspend fun deleteAlertsForLocation(locationId: String)
|
||||
|
||||
@Query("SELECT * FROM alerts")
|
||||
fun getAllLocationsAlerts(): Flow<List<AlertEntity?>>
|
||||
|
||||
}
|
||||
+10
@@ -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?)
|
||||
|
||||
}
|
||||
+24
@@ -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
|
||||
)
|
||||
+34
@@ -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
|
||||
)
|
||||
+32
@@ -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
|
||||
)
|
||||
+41
@@ -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"]
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
+2
-29
@@ -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
|
||||
)
|
||||
+31
@@ -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
|
||||
)
|
||||
}
|
||||
+29
@@ -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
|
||||
}
|
||||
}
|
||||
+35
@@ -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<Alert> {
|
||||
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
|
||||
}
|
||||
}
|
||||
+23
@@ -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
|
||||
)
|
||||
}
|
||||
+6
-6
@@ -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 <T> matchingMinMaxTemperature(
|
||||
|
||||
|
||||
private fun getRainSum(
|
||||
data: Map<Long, Double>,
|
||||
data: Map<Long, Double?>,
|
||||
time: Long
|
||||
): Double {
|
||||
|
||||
@@ -330,7 +330,7 @@ private fun getRainSum(
|
||||
}
|
||||
|
||||
private fun getSnowfallSum(
|
||||
data: Map<Long, Double>,
|
||||
data: Map<Long, Double?>,
|
||||
time: Long
|
||||
): Double {
|
||||
|
||||
@@ -346,16 +346,16 @@ private fun getSnowfallSum(
|
||||
}
|
||||
|
||||
private fun getMinVisibility(
|
||||
data: Map<Long, Double>,
|
||||
data: Map<Long, Double?>,
|
||||
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
|
||||
}
|
||||
|
||||
+23
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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
|
||||
|
||||
}
|
||||
+12
@@ -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
|
||||
}
|
||||
+21
@@ -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<List<Alert?>> {
|
||||
return dao.getAllLocationsAlerts().map { list -> list.map { it?.toDomain() } }
|
||||
}
|
||||
|
||||
suspend fun getAlertsForLocation(locationId: String): List<Alert?> {
|
||||
return dao.getAlertsForLocation(locationId).map { it?.toDomain() }
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -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() } }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
+55
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Alert?> = 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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+36
@@ -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<AlertsScreenUiState> = _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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
-31
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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?"
|
||||
)
|
||||
}
|
||||
}
|
||||
-1
@@ -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,
|
||||
|
||||
+17
-5
@@ -62,11 +62,11 @@ fun LocationsScreen(
|
||||
locations: List<Location>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+8
-2
@@ -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<Weather> = emptyList(),
|
||||
onAddCurrentLocation: () -> Unit,
|
||||
isDeviceLocationLoading: Boolean
|
||||
isDeviceLocationLoading: Boolean,
|
||||
alerts: List<Alert?>
|
||||
) {
|
||||
|
||||
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()
|
||||
)
|
||||
|
||||
|
||||
|
||||
+9
-1
@@ -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)
|
||||
|
||||
+9
-1
@@ -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),
|
||||
|
||||
+6
-1
@@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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<Alert> = 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)
|
||||
|
||||
|
||||
+18
@@ -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,
|
||||
|
||||
+2
-15
@@ -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),
|
||||
|
||||
@@ -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<Alert>,
|
||||
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)}")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+32
-13
@@ -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)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+6
-6
@@ -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") }
|
||||
// )
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
+107
-37
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M109,840Q98,840 89,834.5Q80,829 75,820Q70,811 69.5,800.5Q69,790 75,780L445,140Q451,130 460.5,125Q470,120 480,120Q490,120 499.5,125Q509,130 515,140L885,780Q891,790 890.5,800.5Q890,811 885,820Q880,829 871,834.5Q862,840 851,840L109,840ZM508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720Q497,720 508.5,708.5ZM508.5,588.5Q520,577 520,560L520,440Q520,423 508.5,411.5Q497,400 480,400Q463,400 451.5,411.5Q440,423 440,440L440,560Q440,577 451.5,588.5Q463,600 480,600Q497,600 508.5,588.5Z"/>
|
||||
</vector>
|
||||
@@ -279,8 +279,8 @@
|
||||
<string name="weather_wind_direction_from">Von %1$s</string>
|
||||
<string name="day_length_hr">%1$s Std</string>
|
||||
<string name="error_no_results">Keine Ergebnisse gefunden</string>
|
||||
<string name="summary_overview_template_same_condition">Es wird den ganzen Tag und die ganze Nacht %1$s bleiben.</string>
|
||||
<string name="summary_overview_template_day_night_condition">Am Tag wird es %1$s sein und in der Nacht %2$s werden.</string>
|
||||
<string name="summary_overview_template_same_condition">It will stay %1$s throughout the day and night.</string>
|
||||
<string name="summary_overview_template_day_night_condition">Am Tag wird es %1$s sein und in der Nacht %2$s werden.</string>
|
||||
<string name="summary_rain_template_day_night">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.</string>
|
||||
<string name="summary_rain_template_day">Tagsüber wird gegen %1$s Regen erwartet, mit einer Wahrscheinlichkeit von %2$s.</string>
|
||||
<string name="summary_rain_template_night">Heute Nacht besteht gegen %2$s eine Regenwahrscheinlichkeit von %1$s.</string>
|
||||
|
||||
@@ -284,11 +284,11 @@
|
||||
<string name="summary_rain_template_day_night">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</string>
|
||||
<string name="summary_rain_template_day">Durante el día, se espera lluvia alrededor de las %1$s con una probabilidad de %2$s.</string>
|
||||
<string name="summary_rain_template_night">Esta noche, hay una probabilidad del %1$s que llueva alrededor de las %2$s.</string>
|
||||
<string name="summary_snow_template_day_night"></string>
|
||||
<string name="summary_snow_template_day">Se pronostica nieve alrededor del %1$s durante el día con una probabilidad del %2$s.</string>
|
||||
<string name="summary_snow_template_night">Durante la noche, hay un %1$s de probabilidad de nieve alrededor del %2$s.</string>
|
||||
<string name="summary_temp_template_extreme">Se espera calor extremo durante el día con una máxima de %1$s. Las temperaturas bajarán alrededor de %2$s.</string>
|
||||
<string name="summary_temp_template_warm">Hará calor durante el día, con máximas cercanas a %1$s. Esta noche, las temperaturas bajarán alrededor de %2$s</string>
|
||||
<string name="summary_snow_template_day_night">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.</string>
|
||||
<string name="summary_snow_template_day">Snow is forecast around %1$s during the day with a %2$s chance.</string>
|
||||
<string name="summary_snow_template_night">Overnight, there\'s a %1$s chance of snow around %2$s.</string>
|
||||
<string name="summary_temp_template_extreme">Expect extreme heat during the day with a high of %1$s. Temperatures will ease to around %2$s tonight.</string>
|
||||
<string name="summary_temp_template_warm">It will be warm during the day with highs near %1$s. Tonight, temperatures will fall to around %2$s.</string>
|
||||
<string name="summary_temp_template_mild">A mild day is expected with temperatures reaching %1$s. Overnight, it\'ll cool to around %2$s.</string>
|
||||
<string name="summary_temp_template_cool">Hará frío durante el día, con máximas cercanas a %1$s. Esta noche, las temperaturas bajarán alrededor de %2$s</string>
|
||||
<string name="summary_temp_template_cold">Cold weather continues with daytime temperatures around %1$s. Overnight, lows will fall to around %2$s.</string>
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
<string name="day_length_hr">%1$s tuntia</string>
|
||||
<string name="error_no_results">Tuloksia ei löytynyt</string>
|
||||
<string name="summary_overview_template_same_condition">Luvassa %1$s päivällä ja yöllä.</string>
|
||||
<string name="summary_overview_template_day_night_condition">Päivällä luvassa poutaa, yöksi %2$s.</string>
|
||||
<string name="summary_overview_template_day_night_condition">Päivällä on %1$s, yöllä muuttuu %2$s.</string>
|
||||
<string name="summary_rain_template_day_night">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.</string>
|
||||
<string name="summary_rain_template_day">Päivällä on luvassa sadetta noin kello %1$s %2$s todennäköisyydellä.</string>
|
||||
<string name="summary_rain_template_night">Yöllä on %1$s todennäköisyys sateeseen noin kello %2$s.</string>
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
<string name="weather_wind_direction_from">%1$s风</string>
|
||||
<string name="day_length_hr">%1$s 小时</string>
|
||||
<string name="error_no_results">未找到结果</string>
|
||||
<string name="summary_overview_template_same_condition"></string>
|
||||
<string name="summary_overview_template_same_condition">It will stay %1$s throughout the day and night</string>
|
||||
<string name="summary_overview_template_day_night_condition">日间将以%1$s为主,夜间转为%2$s。</string>
|
||||
<string name="summary_rain_template_day_night">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.</string>
|
||||
<string name="summary_rain_template_day">During the day, rain is expected around %1$s with a %2$s chance.</string>
|
||||
|
||||
@@ -381,6 +381,20 @@
|
||||
<string name="weather_aqi_source">Air quality source</string>
|
||||
<string name="location_name">Location name</string>
|
||||
<string name="location_info">Location information</string>
|
||||
<string name="location_long_press_info">Long press a location for more options</string>
|
||||
|
||||
<string name="alert_severity_critical">Critical</string>
|
||||
<string name="alert_severity_high">High</string>
|
||||
<string name="alert_severity_medium">Medium</string>
|
||||
<string name="alert_severity_low">Low</string>
|
||||
<string name="alert_severity_unknown">Unknown</string>
|
||||
|
||||
<string name="confirm_delete_location">Delete Location</string>
|
||||
<string name="alert_delete_location_secondary">Are you sure you want to delete this location?</string>
|
||||
|
||||
|
||||
<string name="weather_alerts">Alerts</string>
|
||||
|
||||
</resources>
|
||||
|
||||
<!--<string name=""></string>-->
|
||||
Reference in new issue
Block a user