├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-playstore.png │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── arrays.xml │ │ │ │ ├── preloaded_fonts.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── themes.xml │ │ │ │ ├── strings.xml │ │ │ │ └── font_certs.xml │ │ │ ├── drawable │ │ │ │ ├── indicator_dot_style.xml │ │ │ │ ├── current_hourly_weather_background.xml │ │ │ │ ├── hourly_weather_box_background.xml │ │ │ │ ├── tomorrow_weather_box_background.xml │ │ │ │ ├── search_location_input_style.xml │ │ │ │ ├── rounded_blurry_box.xml │ │ │ │ ├── home_background.xml │ │ │ │ ├── icon_music.xml │ │ │ │ ├── icon_add.xml │ │ │ │ ├── icon_menu.xml │ │ │ │ ├── icon_right_arrow.xml │ │ │ │ ├── icon_back.xml │ │ │ │ ├── icon_search.xml │ │ │ │ ├── icon_dropdown.xml │ │ │ │ ├── icon_humidity.xml │ │ │ │ ├── icon_weather_sun.xml │ │ │ │ ├── icon_weather_cloud.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ ├── icon_weather_cloud_fog.xml │ │ │ │ ├── icon_weather_rain_cloud.xml │ │ │ │ ├── icon_weather_sun_cloud.xml │ │ │ │ ├── icon_wind.xml │ │ │ │ ├── icon_sunrise.xml │ │ │ │ ├── icon_sunset.xml │ │ │ │ ├── icon_weather_sun_cloud_rain.xml │ │ │ │ ├── icon_weather_sun_rain_cloud.xml │ │ │ │ ├── icon_pressure.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── icon_weather_thunderstorm_cloud.xml │ │ │ │ └── icon_weather_snow_cloud.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── menu │ │ │ │ ├── atmospheric_pressure_unit_menu.xml │ │ │ │ ├── temperature_unit_menu.xml │ │ │ │ └── windsped_unit_menu.xml │ │ │ ├── font │ │ │ │ ├── inter.xml │ │ │ │ ├── inter_bold.xml │ │ │ │ ├── inter_light.xml │ │ │ │ ├── inter_medium.xml │ │ │ │ └── inter_semibold.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── layout_hourly_weather_item.xml │ │ │ │ ├── layout_next_weather_item.xml │ │ │ │ ├── cities_item_layout.xml │ │ │ │ ├── fragment_search.xml │ │ │ │ └── fragment_next_7_days.xml │ │ │ └── navigation │ │ │ │ └── app_nav_graph.xml │ │ ├── java │ │ │ └── me │ │ │ │ └── tangobee │ │ │ │ └── weathernaut │ │ │ │ ├── models │ │ │ │ ├── NewLocationModel.kt │ │ │ │ ├── DailyWeatherRVModel.kt │ │ │ │ ├── HourlyWeatherRVModel.kt │ │ │ │ ├── WeatherData │ │ │ │ │ ├── DailyWeather │ │ │ │ │ │ ├── DailyWeather.kt │ │ │ │ │ │ ├── DailyUnits.kt │ │ │ │ │ │ └── Daily.kt │ │ │ │ │ ├── HourlyWeather │ │ │ │ │ │ ├── HourlyWeather.kt │ │ │ │ │ │ ├── HourlyUnits.kt │ │ │ │ │ │ └── Hourly.kt │ │ │ │ │ ├── CurrentWeather │ │ │ │ │ │ ├── CurrentWeather.kt │ │ │ │ │ │ ├── Current.kt │ │ │ │ │ │ └── CurrentUnits.kt │ │ │ │ │ └── WeatherData.kt │ │ │ │ ├── GeocodingData │ │ │ │ │ ├── GeocodingModel.kt │ │ │ │ │ └── GeocodingResult.kt │ │ │ │ ├── GeoWeatherModel.kt │ │ │ │ └── SettingsModel.kt │ │ │ │ ├── data │ │ │ │ ├── api │ │ │ │ │ ├── GeocodingService.kt │ │ │ │ │ └── WeatherService.kt │ │ │ │ ├── RetrofitHelper.kt │ │ │ │ └── repository │ │ │ │ │ ├── GeocodingRepository.kt │ │ │ │ │ └── WeatherRepository.kt │ │ │ │ ├── viewmodels │ │ │ │ ├── WeatherViewModelFactory.kt │ │ │ │ ├── GeocodingViewModelFactory.kt │ │ │ │ ├── GeocodingViewModel.kt │ │ │ │ └── WeatherViewModel.kt │ │ │ │ ├── constants │ │ │ │ ├── UnitsMapper.kt │ │ │ │ ├── WeatherImageMapper.kt │ │ │ │ └── WeatherConstants.kt │ │ │ │ ├── utils │ │ │ │ ├── UnitsCalculator.kt │ │ │ │ ├── GeocodingHelper.kt │ │ │ │ ├── SharedPreferencesHelper.kt │ │ │ │ └── WeatherHelper.kt │ │ │ │ ├── ui │ │ │ │ ├── adapter │ │ │ │ │ ├── DailyWeatherRVAdapter.kt │ │ │ │ │ ├── HourlyWeatherRVAdapter.kt │ │ │ │ │ └── LocationRVAdapter.kt │ │ │ │ ├── Next7DaysFragment.kt │ │ │ │ ├── SearchFragment.kt │ │ │ │ ├── HomeFragment.kt │ │ │ │ └── SettingsFragment.kt │ │ │ │ ├── services │ │ │ │ └── WeatherMusicService.kt │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── me │ │ │ └── tangobee │ │ │ └── weathernaut │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── me │ │ └── tangobee │ │ └── weathernaut │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── assets ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── Flat-cover.png └── direct-apk-download.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── vcs.xml ├── compiler.xml ├── kotlinc.xml ├── discord.xml ├── misc.xml └── gradle.xml ├── .gitignore ├── settings.gradle.kts ├── LICENSE ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /assets/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/assets/1.jpg -------------------------------------------------------------------------------- /assets/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/assets/2.jpg -------------------------------------------------------------------------------- /assets/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/assets/3.jpg -------------------------------------------------------------------------------- /assets/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/assets/4.jpg -------------------------------------------------------------------------------- /assets/Flat-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/assets/Flat-cover.png -------------------------------------------------------------------------------- /assets/direct-apk-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/assets/direct-apk-download.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/NewLocationModel.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models 2 | 3 | data class NewLocationModel( 4 | val query: String 5 | ) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangoBeee/Weathernaut/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #39FDD6 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/DailyWeatherRVModel.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models 2 | 3 | data class DailyWeatherRVModel(val day: String, val weatherTemp: String, val weatherIcon: Int) 4 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/HourlyWeatherRVModel.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models 2 | 3 | data class HourlyWeatherRVModel(val time: String, val weatherIcon: Int, val weatherTemp: String) 4 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/DailyWeather/DailyWeather.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.DailyWeather 2 | 3 | data class DailyWeather( 4 | val daily: Daily, 5 | val daily_units: DailyUnits 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/GeocodingData/GeocodingModel.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.GeocodingData 2 | 3 | data class GeocodingModel( 4 | val generationtime_ms: Double, 5 | val results: ArrayList? 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/HourlyWeather/HourlyWeather.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.HourlyWeather 2 | 3 | data class HourlyWeather( 4 | val hourly: Hourly, 5 | val hourly_units: HourlyUnits 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/GeoWeatherModel.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models 2 | 3 | data class GeoWeatherModel( 4 | val ll: List, 5 | val city: String, 6 | val country: String, 7 | val timezone: String 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/CurrentWeather/CurrentWeather.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.CurrentWeather 2 | 3 | data class CurrentWeather( 4 | val current: Current, 5 | val current_units: CurrentUnits 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/HourlyWeather/HourlyUnits.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.HourlyWeather 2 | 3 | data class HourlyUnits( 4 | var temperature_2m: String, 5 | val time: String, 6 | val weather_code: String 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/HourlyWeather/Hourly.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.HourlyWeather 2 | 3 | data class Hourly( 4 | var temperature_2m: List, 5 | val time: List, 6 | val weather_code: List 7 | ) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 09 17:54:46 IST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/indicator_dot_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/SettingsModel.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models 2 | 3 | data class SettingsModel( 4 | var tempUnit: String = "celsius", 5 | var windSpeedUnit: String = "kmh", 6 | var pressureUnit: String = "hpa", 7 | var isMusicOn: Boolean = true 8 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/current_hourly_weather_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/hourly_weather_box_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tomorrow_weather_box_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/search_location_input_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reply 5 | Reply to all 6 | 7 | 8 | 9 | reply 10 | reply_all 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/atmospheric_pressure_unit_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/temperature_unit_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/DailyWeather/DailyUnits.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.DailyWeather 2 | 3 | data class DailyUnits( 4 | val sunrise: String, 5 | val sunset: String, 6 | var temperature_2m_max: String, 7 | var temperature_2m_min: String, 8 | val time: String, 9 | val weather_code: String 10 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_blurry_box.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/DailyWeather/Daily.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.DailyWeather 2 | 3 | data class Daily( 4 | val sunrise: List, 5 | val sunset: List, 6 | var temperature_2m_max: List, 7 | var temperature_2m_min: List, 8 | val time: List, 9 | val weather_code: List 10 | ) -------------------------------------------------------------------------------- /app/src/main/res/values/preloaded_fonts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @font/inter 5 | @font/inter_bold 6 | @font/inter_light 7 | @font/inter_medium 8 | @font/inter_semibold 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/CurrentWeather/Current.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.CurrentWeather 2 | 3 | data class Current( 4 | val interval: Int, 5 | var pressure_msl: Double, 6 | val relative_humidity_2m: Int, 7 | var temperature_2m: Double, 8 | val time: String, 9 | val weather_code: Int, 10 | var wind_speed_10m: Double 11 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_bold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "Weathernaut" 17 | include(":app") 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_music.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_medium.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_semibold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/CurrentWeather/CurrentUnits.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData.CurrentWeather 2 | 3 | data class CurrentUnits( 4 | val interval: String, 5 | var pressure_msl: String, 6 | val relative_humidity_2m: String, 7 | var temperature_2m: String, 8 | val time: String, 9 | val weather_code: String, 10 | var wind_speed_10m: String 11 | ) -------------------------------------------------------------------------------- /app/src/test/java/me/tangobee/weathernaut/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | 6 | #313341 7 | #9C9EAA 8 | #808080 9 | #D6996B 10 | #E2A272 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_add.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/menu/windsped_unit_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_menu.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/data/api/GeocodingService.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.data.api 2 | 3 | import me.tangobee.weathernaut.models.GeocodingData.GeocodingModel 4 | import me.tangobee.weathernaut.models.NewLocationModel 5 | import retrofit2.Response 6 | import retrofit2.http.Body 7 | import retrofit2.http.POST 8 | 9 | interface GeocodingService { 10 | 11 | @POST("/api/geocoding") 12 | suspend fun getLocations(@Body newLocationDataModel: NewLocationModel) : Response 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_right_arrow.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/viewmodels/WeatherViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import me.tangobee.weathernaut.data.repository.WeatherRepository 6 | 7 | class WeatherViewModelFactory(private val weatherRepository: WeatherRepository) : ViewModelProvider.Factory { 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | override fun create(modelClass: Class): T { 11 | return WeatherViewModel(weatherRepository) as T 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/data/api/WeatherService.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.data.api 2 | 3 | import me.tangobee.weathernaut.models.GeoWeatherModel 4 | import me.tangobee.weathernaut.models.WeatherData.WeatherData 5 | import retrofit2.Response 6 | import retrofit2.http.Body 7 | import retrofit2.http.POST 8 | 9 | interface WeatherService { 10 | 11 | @POST("/api/weather") 12 | suspend fun getWeather() : Response 13 | 14 | @POST("/api/weather") 15 | suspend fun getGeoWeather(@Body geoWeatherModel: GeoWeatherModel) : Response 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/viewmodels/GeocodingViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import me.tangobee.weathernaut.data.repository.GeocodingRepository 6 | 7 | class GeocodingViewModelFactory(private val geocodingRepository: GeocodingRepository) : ViewModelProvider.Factory { 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | override fun create(modelClass: Class): T { 11 | return GeocodingViewModel(geocodingRepository) as T 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_back.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/WeatherData/WeatherData.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.WeatherData 2 | 3 | import me.tangobee.weathernaut.models.WeatherData.CurrentWeather.CurrentWeather 4 | import me.tangobee.weathernaut.models.WeatherData.DailyWeather.DailyWeather 5 | import me.tangobee.weathernaut.models.WeatherData.HourlyWeather.HourlyWeather 6 | 7 | data class WeatherData( 8 | val city: String, 9 | val country: String, 10 | val lat: Double, 11 | val long: Double, 12 | val current_weather: CurrentWeather, 13 | val daily_weather: DailyWeather, 14 | val hourly_weather: HourlyWeather 15 | ) -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_dropdown.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/models/GeocodingData/GeocodingResult.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.models.GeocodingData 2 | 3 | data class GeocodingResult( 4 | val admin1: String, 5 | val admin1_id: Int, 6 | val admin2: String, 7 | val admin2_id: Int, 8 | val admin3: String, 9 | val admin3_id: Int, 10 | val admin4: String, 11 | val admin4_id: Int, 12 | val country: String, 13 | val country_code: String, 14 | val country_id: Int, 15 | val elevation: Int, 16 | val feature_code: String, 17 | val id: Int, 18 | val latitude: Double, 19 | val longitude: Double, 20 | val name: String, 21 | val population: Int, 22 | val timezone: String 23 | ) -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/constants/UnitsMapper.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.constants 2 | 3 | object UnitsMapper { 4 | private val unitMap: Map = mapOf( 5 | "°C" to "celsius", 6 | "°F" to "fahrenheit", 7 | "km/h" to "kmh", 8 | "mph" to "mph", 9 | "m/s" to "ms", 10 | "hPa" to "hpa", 11 | "atm" to "atm" 12 | ) 13 | 14 | private val reverseUnitMap: Map = unitMap.entries.associate { (key, value) -> value to key } 15 | 16 | fun getFullUnit(shorthand: String): String { 17 | return unitMap.getOrDefault(shorthand, shorthand) 18 | } 19 | 20 | fun getShorthandUnit(fullUnit: String): String { 21 | return reverseUnitMap.getOrDefault(fullUnit, fullUnit) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/androidTest/java/me/tangobee/weathernaut/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("me.tangobee.weathernaut", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_humidity.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/data/RetrofitHelper.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.data 2 | 3 | import okhttp3.OkHttpClient 4 | import retrofit2.Retrofit 5 | import retrofit2.converter.gson.GsonConverterFactory 6 | import java.util.concurrent.TimeUnit 7 | 8 | object RetrofitHelper { 9 | 10 | private const val BASE_URL = "https://weathernaut-backend.onrender.com" 11 | 12 | fun getInstance() : Retrofit { 13 | val okHttpClient = OkHttpClient.Builder() 14 | .connectTimeout(1, TimeUnit.MINUTES) 15 | .readTimeout(30, TimeUnit.SECONDS) 16 | .writeTimeout(15, TimeUnit.SECONDS) 17 | .retryOnConnectionFailure(true) 18 | .build() 19 | 20 | 21 | return Retrofit.Builder() 22 | .baseUrl(BASE_URL) 23 | .client(okHttpClient) 24 | .addConverterFactory(GsonConverterFactory.create()) 25 | .build() 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_sun.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/constants/WeatherImageMapper.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.constants 2 | 3 | import me.tangobee.weathernaut.R 4 | 5 | class WeatherImageMapper { 6 | companion object { 7 | fun getImageForWeatherCode(weatherCode: Int): Int { 8 | return when (weatherCode) { 9 | 0, 1 -> R.drawable.icon_weather_sun 10 | 2, 3 -> R.drawable.icon_weather_sun_cloud 11 | 45, 48 -> R.drawable.icon_weather_cloud_fog 12 | in 51..55, in 56..57 -> R.drawable.icon_weather_rain_cloud 13 | in 61..65, in 66..67 -> R.drawable.icon_weather_rain_cloud 14 | 71, 73, 75, 77 -> R.drawable.icon_weather_snow_cloud 15 | 80, 81, 82 -> R.drawable.icon_weather_sun_cloud_rain 16 | 85, 86 -> R.drawable.icon_weather_snow_cloud 17 | 95, 96, 99 -> R.drawable.icon_weather_thunderstorm_cloud 18 | else -> R.drawable.icon_weather_cloud 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/data/repository/GeocodingRepository.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.data.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import me.tangobee.weathernaut.data.api.GeocodingService 6 | import me.tangobee.weathernaut.models.GeocodingData.GeocodingModel 7 | import me.tangobee.weathernaut.models.NewLocationModel 8 | 9 | class GeocodingRepository(private val geocodingService: GeocodingService) { 10 | 11 | private val locationsLiveData = MutableLiveData() 12 | 13 | val locationsData: LiveData 14 | get() = locationsLiveData 15 | 16 | suspend fun getLocations(newLocationDataModel: NewLocationModel) { 17 | val locationResult = geocodingService.getLocations(newLocationDataModel) 18 | 19 | if(locationResult.isSuccessful && locationResult.body() != null) { 20 | locationsLiveData.postValue(locationResult.body()) 21 | } else { 22 | locationsLiveData.postValue(null) 23 | } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/viewmodels/GeocodingViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.viewmodels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.CoroutineExceptionHandler 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | import me.tangobee.weathernaut.data.repository.GeocodingRepository 10 | import me.tangobee.weathernaut.models.GeocodingData.GeocodingModel 11 | import me.tangobee.weathernaut.models.NewLocationModel 12 | 13 | class GeocodingViewModel(private val geocodingRepository: GeocodingRepository) : ViewModel() { 14 | 15 | fun getLocations(newLocationDataModel: NewLocationModel, coroutineExceptionHandler: CoroutineExceptionHandler) { 16 | viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) { 17 | geocodingRepository.getLocations(newLocationDataModel) 18 | } 19 | } 20 | 21 | val locationLiveData : LiveData 22 | get() = geocodingRepository.locationsData 23 | 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TangoBee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/viewmodels/WeatherViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.viewmodels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.CoroutineExceptionHandler 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | import me.tangobee.weathernaut.data.repository.WeatherRepository 10 | import me.tangobee.weathernaut.models.GeoWeatherModel 11 | import me.tangobee.weathernaut.models.WeatherData.WeatherData 12 | 13 | class WeatherViewModel(private val weatherRepository: WeatherRepository) : ViewModel() { 14 | 15 | fun getWeather(coroutineExceptionHandler: CoroutineExceptionHandler) { 16 | viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) { 17 | weatherRepository.getWeather() 18 | } 19 | } 20 | 21 | fun getGeoWeather(coroutineExceptionHandler: CoroutineExceptionHandler, geoWeatherModel: GeoWeatherModel) { 22 | viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) { 23 | weatherRepository.getGeoWeather(geoWeatherModel) 24 | } 25 | } 26 | 27 | fun updateWeatherData(weatherData: WeatherData) { 28 | weatherRepository.updateWeatherData(weatherData) 29 | } 30 | 31 | val weatherLiveData : LiveData 32 | get() = weatherRepository.weatherData 33 | 34 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/data/repository/WeatherRepository.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.data.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import me.tangobee.weathernaut.data.api.WeatherService 6 | import me.tangobee.weathernaut.models.GeoWeatherModel 7 | import me.tangobee.weathernaut.models.WeatherData.WeatherData 8 | 9 | class WeatherRepository(private val weatherService: WeatherService) { 10 | 11 | private val weatherLiveData = MutableLiveData() 12 | 13 | val weatherData: LiveData 14 | get() = weatherLiveData 15 | 16 | suspend fun getWeather() { 17 | val weatherResult = weatherService.getWeather() 18 | 19 | if(weatherResult.isSuccessful && weatherResult.body() != null) { 20 | weatherLiveData.postValue(weatherResult.body()) 21 | } else { 22 | weatherLiveData.postValue(null) 23 | } 24 | } 25 | 26 | suspend fun getGeoWeather(geoWeatherModel: GeoWeatherModel) { 27 | val weatherResult = weatherService.getGeoWeather(geoWeatherModel) 28 | 29 | if(weatherResult.isSuccessful && weatherResult.body() != null) { 30 | weatherLiveData.postValue(weatherResult.body()) 31 | } else { 32 | weatherLiveData.postValue(null) 33 | } 34 | } 35 | 36 | fun updateWeatherData(weatherData: WeatherData) { 37 | weatherLiveData.postValue(weatherData) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/utils/UnitsCalculator.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.utils 2 | 3 | class UnitConverter { 4 | 5 | fun convertTemperature(value: Double, fromUnit: String, toUnit: String): Double { 6 | val result = when (fromUnit to toUnit) { 7 | "celsius" to "fahrenheit" -> (value * 9 / 5) + 32 8 | "fahrenheit" to "celsius" -> (value - 32) * 5 / 9 9 | else -> value 10 | } 11 | 12 | return result.formatToTwoDecimalPlaces() 13 | } 14 | 15 | fun convertWindSpeed(value: Double, fromUnit: String, toUnit: String): Double { 16 | val result = when (fromUnit to toUnit) { 17 | "kmh" to "mph" -> value / 1.60934 18 | "mph" to "kmh" -> value * 1.60934 19 | "kmh" to "ms" -> value / 3.6 20 | "ms" to "kmh" -> value * 3.6 21 | "mph" to "ms" -> value * 0.44704 22 | "ms" to "mph" -> value / 0.44704 23 | else -> value 24 | } 25 | 26 | return result.formatToTwoDecimalPlaces() 27 | } 28 | 29 | fun convertPressure(value: Double, fromUnit: String, toUnit: String): Double { 30 | val result = when (fromUnit to toUnit) { 31 | "hpa" to "atm" -> value / 1013.25 32 | "atm" to "hpa" -> value * 1013.25 33 | else -> value 34 | } 35 | 36 | return result.formatToTwoDecimalPlaces() 37 | } 38 | 39 | private fun Double.formatToTwoDecimalPlaces(): Double { 40 | return String.format("%.2f", this).toDouble() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/ui/adapter/DailyWeatherRVAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.ui.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import androidx.recyclerview.widget.RecyclerView 9 | import me.tangobee.weathernaut.R 10 | import me.tangobee.weathernaut.models.DailyWeatherRVModel 11 | 12 | class DailyWeatherRVAdapter(private val dailyWeatherRVModelList: ArrayList) : RecyclerView.Adapter() { 13 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 14 | return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_next_weather_item, parent, false)) 15 | } 16 | 17 | override fun getItemCount(): Int { 18 | return dailyWeatherRVModelList.size 19 | } 20 | 21 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 22 | holder.nextWeatherDay.text = dailyWeatherRVModelList[position].day 23 | holder.nextWeatherTemp.text = dailyWeatherRVModelList[position].weatherTemp 24 | holder.nextWeatherIcon.setImageResource(dailyWeatherRVModelList[position].weatherIcon) 25 | } 26 | 27 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 28 | val nextWeatherIcon: ImageView = itemView.findViewById(R.id.nextWeatherIcon) 29 | val nextWeatherDay: TextView = itemView.findViewById(R.id.nextWeatherDay) 30 | val nextWeatherTemp: TextView = itemView.findViewById(R.id.nextWeatherTemp) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_cloud.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_hourly_weather_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 22 | 23 | 30 | 31 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/constants/WeatherConstants.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.constants 2 | 3 | data class WeatherConstants( 4 | val code: Int, 5 | val description: String 6 | ) 7 | 8 | object WeatherCodes { 9 | val weatherConstants = listOf( 10 | WeatherConstants(0, "Clear"), 11 | WeatherConstants(1, "Clear/Partly Cloudy"), 12 | WeatherConstants(2, "Partly Cloudy"), 13 | WeatherConstants(3, "Overcast"), 14 | WeatherConstants(45, "Fog"), 15 | WeatherConstants(48, "Rime Fog"), 16 | WeatherConstants(51, "Light Drizzle"), 17 | WeatherConstants(53, "Moderate Drizzle"), 18 | WeatherConstants(55, "Heavy Drizzle"), 19 | WeatherConstants(56, "Light Freezing Drizzle"), 20 | WeatherConstants(57, "Heavy Freezing Drizzle"), 21 | WeatherConstants(61, "Light Rain"), 22 | WeatherConstants(63, "Moderate Rain"), 23 | WeatherConstants(65, "Heavy Rain"), 24 | WeatherConstants(66, "Light Freezing Rain"), 25 | WeatherConstants(67, "Heavy Freezing Rain"), 26 | WeatherConstants(71, "Light Snow"), 27 | WeatherConstants(73, "Moderate Snow"), 28 | WeatherConstants(75, "Heavy Snow"), 29 | WeatherConstants(77, "Snow Grains"), 30 | WeatherConstants(80, "Light Rain Showers"), 31 | WeatherConstants(81, "Moderate Rain Showers"), 32 | WeatherConstants(82, "Heavy Rain Showers"), 33 | WeatherConstants(85, "Light Snow Showers"), 34 | WeatherConstants(86, "Heavy Snow Showers"), 35 | WeatherConstants(95, "Slight Thunderstorm"), 36 | WeatherConstants(96, "Slight Thunderstorm"), 37 | WeatherConstants(99, "Heavy Thunderstorm") 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/utils/GeocodingHelper.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.utils 2 | 3 | import me.tangobee.weathernaut.models.GeocodingData.GeocodingResult 4 | import kotlin.math.* 5 | 6 | class GeocodingHelper { 7 | companion object { 8 | fun areLocationsSame( 9 | lat1: Double, 10 | lon1: Double, 11 | lat2: Double, 12 | lon2: Double, 13 | threshold: Double = 0.01 14 | ): Boolean { 15 | return abs(lat1 - lat2) < threshold && abs(lon1 - lon2) < threshold 16 | } 17 | 18 | // Haversine formula to calculate distance between two lat/lng points in kilometers 19 | private fun calculateDistance( 20 | userLat: Double, userLng: Double, 21 | placeLat: Double, placeLng: Double 22 | ): Double { 23 | val earthRadiusKm = 6371.0 24 | 25 | val dLat = Math.toRadians(placeLat - userLat) 26 | val dLng = Math.toRadians(placeLng - userLng) 27 | 28 | val a = sin(dLat / 2) * sin(dLat / 2) + 29 | cos(Math.toRadians(userLat)) * cos(Math.toRadians(placeLat)) * 30 | sin(dLng / 2) * sin(dLng / 2) 31 | 32 | val c = 2 * atan2(sqrt(a), sqrt(1 - a)) 33 | 34 | return earthRadiusKm * c 35 | } 36 | 37 | fun sortLocationsByProximity( 38 | userLat: Double, userLng: Double, 39 | locationRVModelList: MutableList 40 | ): List { 41 | return locationRVModelList.sortedBy { location -> 42 | calculateDistance(userLat, userLng, location.latitude, location.longitude) 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/app_nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 16 | 19 | 22 | 23 | 28 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_next_weather_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 32 | 33 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_cloud_fog.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/cities_item_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 31 | 32 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/utils/SharedPreferencesHelper.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import com.google.gson.Gson 6 | import me.tangobee.weathernaut.models.GeoWeatherModel 7 | import me.tangobee.weathernaut.models.SettingsModel 8 | 9 | class SharedPreferencesHelper(context: Context) { 10 | companion object { 11 | private const val PREFS_NAME = "WeathernautPrefs" 12 | private const val SETTINGS_KEY = "SettingsKey" 13 | private const val GEOCODING_KEY = "GeocodingKey" 14 | } 15 | 16 | private val sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) 17 | private val gson = Gson() 18 | 19 | fun saveSettings(settings: SettingsModel) { 20 | val settingsJson = gson.toJson(settings) 21 | sharedPreferences.edit().putString(SETTINGS_KEY, settingsJson).apply() 22 | } 23 | 24 | fun getSettings(): SettingsModel? { 25 | val settingsJson = sharedPreferences.getString(SETTINGS_KEY, null) 26 | return if (settingsJson != null) { 27 | gson.fromJson(settingsJson, SettingsModel::class.java) 28 | } else { 29 | null 30 | } 31 | } 32 | 33 | fun updateSettings(updateFunction: (SettingsModel) -> SettingsModel) { 34 | val currentSettings = getSettings() 35 | if (currentSettings != null) { 36 | val updatedSettings = updateFunction.invoke(currentSettings) 37 | saveSettings(updatedSettings) 38 | } 39 | } 40 | 41 | fun saveGeocodingData(geoWeatherModel: GeoWeatherModel) { 42 | val geocodingJson = gson.toJson(geoWeatherModel) 43 | sharedPreferences.edit().putString(GEOCODING_KEY, geocodingJson).apply() 44 | } 45 | 46 | fun getGeocodingData(): GeoWeatherModel? { 47 | val geocodingJson = sharedPreferences.getString(GEOCODING_KEY, null) 48 | return if (geocodingJson != null) { 49 | gson.fromJson(geocodingJson, GeoWeatherModel::class.java) 50 | } else { 51 | null 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_rain_cloud.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 30 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_sun_cloud.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_wind.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 16 | 19 | 22 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/ui/adapter/HourlyWeatherRVAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.ui.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.LinearLayout 8 | import android.widget.TextView 9 | import androidx.core.content.ContextCompat 10 | import androidx.core.content.res.ResourcesCompat 11 | import androidx.recyclerview.widget.RecyclerView 12 | import me.tangobee.weathernaut.R 13 | import me.tangobee.weathernaut.models.HourlyWeatherRVModel 14 | 15 | class HourlyWeatherRVAdapter(private val hourlyWeatherRVModelList: ArrayList) : RecyclerView.Adapter() { 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 17 | return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_hourly_weather_item, parent, false)) 18 | } 19 | 20 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 21 | val context = holder.hourlyWeatherContainer.context 22 | holder.weatherIcon.setImageResource(hourlyWeatherRVModelList[position].weatherIcon) 23 | holder.weatherValue.text = hourlyWeatherRVModelList[position].weatherTemp 24 | 25 | val time = hourlyWeatherRVModelList[position].time 26 | if(time == "now") { 27 | val boldTypeface = ResourcesCompat.getFont(context, R.font.inter_semibold) 28 | holder.weatherTime.setTextColor(ContextCompat.getColor(context, R.color.textColor)) 29 | holder.weatherTime.typeface = boldTypeface 30 | holder.hourlyWeatherContainer.background = ContextCompat.getDrawable(context, R.drawable.current_hourly_weather_background) 31 | } else { 32 | val typeface = ResourcesCompat.getFont(context, R.font.inter) 33 | holder.weatherTime.setTextColor(ContextCompat.getColor(context, R.color.upcomingTimeColor)) 34 | holder.weatherTime.typeface = typeface 35 | holder.hourlyWeatherContainer.background = ContextCompat.getDrawable(context, R.drawable.hourly_weather_box_background) 36 | } 37 | 38 | holder.weatherTime.text = time 39 | } 40 | 41 | override fun getItemCount(): Int { 42 | return hourlyWeatherRVModelList.size 43 | } 44 | 45 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 46 | val hourlyWeatherContainer: LinearLayout = itemView.findViewById(R.id.hourlyWeatherContainer) 47 | val weatherTime: TextView = itemView.findViewById(R.id.weatherTime) 48 | val weatherIcon: ImageView = itemView.findViewById(R.id.weatherIcon) 49 | val weatherValue: TextView = itemView.findViewById(R.id.weatherValue) 50 | } 51 | } -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "me.tangobee.weathernaut" 8 | compileSdk = 35 9 | 10 | defaultConfig { 11 | applicationId = "me.tangobee.weathernaut" 12 | minSdk = 24 13 | targetSdk = 35 14 | versionCode = 10 15 | versionName = "2.0" 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildFeatures { 21 | viewBinding = true 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = false 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_1_8 35 | targetCompatibility = JavaVersion.VERSION_1_8 36 | } 37 | kotlinOptions { 38 | jvmTarget = "1.8" 39 | } 40 | } 41 | 42 | dependencies { 43 | 44 | implementation("androidx.core:core-ktx:1.12.0") 45 | implementation("androidx.appcompat:appcompat:1.6.1") 46 | implementation("com.google.android.material:material:1.11.0") 47 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 48 | testImplementation("junit:junit:4.13.2") 49 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 50 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 51 | implementation("androidx.fragment:fragment-ktx:1.6.2") 52 | 53 | // SplashScreen API 54 | implementation("androidx.core:core-splashscreen:1.0.1") 55 | 56 | // ViewModel Lib 57 | val lifecycleVersion = "2.7.0" 58 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") 59 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") 60 | implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion") 61 | 62 | // Retrofit and GSON Lib 63 | implementation("com.squareup.retrofit2:retrofit:2.11.0") 64 | implementation("com.squareup.retrofit2:converter-gson:2.11.0") 65 | 66 | // Google PlayService Location Lib 67 | implementation("com.google.android.gms:play-services-location:21.2.0") 68 | 69 | // Jetpack Navigation Lib 70 | val nav_version = "2.7.7" 71 | implementation("androidx.navigation:navigation-fragment-ktx:$nav_version") 72 | implementation("androidx.navigation:navigation-ui-ktx:$nav_version") 73 | 74 | // Responsive Unit Lib for resizing elements 75 | implementation("com.intuit.ssp:ssp-android:1.1.1") 76 | implementation("com.intuit.sdp:sdp-android:1.1.1") 77 | 78 | // Custom Switch Lib 79 | implementation("io.github.bitvale:switcher:1.1.2") 80 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 23 | 24 | 25 | 26 | 36 | 37 | 49 | 50 | 60 | 61 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_sunrise.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 12 | 16 | 19 | 22 | 25 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_sunset.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 12 | 16 | 19 | 22 | 25 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/services/WeatherMusicService.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.services 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.Service 7 | import android.content.Intent 8 | import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK 9 | import android.media.MediaPlayer 10 | import android.os.IBinder 11 | import android.os.Build 12 | import androidx.core.app.NotificationCompat 13 | import me.tangobee.weathernaut.R 14 | 15 | class WeatherMusicService : Service() { 16 | 17 | companion object { 18 | private var mediaPlayer: MediaPlayer? = null 19 | } 20 | 21 | private val musicUrl = "https://weathernaut-backend.onrender.com/api/music" 22 | private val SERVICE_ID = 1 23 | 24 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 25 | createNotificationChannel() 26 | 27 | // Foreground notification 28 | val notification: Notification = NotificationCompat.Builder(this, "MUSIC_SERVICE_CHANNEL") 29 | .setContentTitle("Playing Weather Music") 30 | .setContentText("Enjoy the music and your day's weather!") 31 | .setSmallIcon(R.drawable.icon_music) 32 | .build() 33 | 34 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 35 | startForeground(SERVICE_ID, notification) 36 | } else { 37 | startForeground(SERVICE_ID, notification, 38 | FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) 39 | } 40 | 41 | if (mediaPlayer == null) { 42 | mediaPlayer = MediaPlayer().apply { 43 | setDataSource(musicUrl) 44 | prepareAsync() 45 | setOnPreparedListener { 46 | it.start() 47 | } 48 | setOnCompletionListener { 49 | start() 50 | } 51 | setOnErrorListener { _, _, _ -> 52 | stopSelf() 53 | false 54 | } 55 | } 56 | } else if (!mediaPlayer!!.isPlaying) { 57 | mediaPlayer!!.start() 58 | } 59 | 60 | return START_STICKY 61 | } 62 | 63 | override fun onDestroy() { 64 | super.onDestroy() 65 | if (mediaPlayer != null) { 66 | mediaPlayer?.stop() 67 | mediaPlayer?.release() 68 | mediaPlayer = null 69 | } 70 | } 71 | 72 | override fun onBind(intent: Intent?): IBinder? { 73 | return null // We don't bind this service to an activity 74 | } 75 | 76 | private fun createNotificationChannel() { 77 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 78 | val serviceChannel = NotificationChannel( 79 | "MUSIC_SERVICE_CHANNEL", 80 | "Music Playback Channel", 81 | NotificationManager.IMPORTANCE_LOW 82 | ) 83 | val manager = getSystemService(NotificationManager::class.java) 84 | manager?.createNotificationChannel(serviceChannel) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Weathernaut 3 | 4 | Something went wrong!\nDon\'t worry its not you, its us. Try later. 5 | No Internet Connection 6 | Search Cities Button 7 | App Settings Button 8 | Atmospheric Pressure Icon 9 | Back Button 10 | Next 7 Days 11 | Tomorrow 12 | Weather Icon 13 | 14 | Messages 15 | Sync 16 | 17 | 18 | Your signature 19 | Default reply action 20 | 21 | 22 | Sync email periodically 23 | Download incoming attachments 24 | Automatically download attachments for incoming emails 25 | 26 | Only download attachments when manually requested 27 | Hello blank fragment 28 | Pressure 29 | Wind Speed 30 | Humidity 31 | Today 32 | Sunset icon 33 | Sunrise icon 34 | Settings 35 | Units 36 | Temperature units 37 | °C 38 | °F 39 | Wind speed units 40 | hPa 41 | atm 42 | Atmospheric pressure units 43 | m/s 44 | km/h 45 | mph 46 | Music according to the weather. 47 | About Weathernaut 48 | Feedback 49 | Privacy Policy 50 | Credits 51 | Other Settings 52 | Weather music 53 | Donate Us 54 | Cancel 55 | Search Your City 56 | Add remove button 57 | Country 58 | City 59 | No locations found 60 | Loading… 61 | Something went wrong! 62 | -------------------------------------------------------------------------------- /app/src/main/res/values/font_certs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @array/com_google_android_gms_fonts_certs_dev 5 | @array/com_google_android_gms_fonts_certs_prod 6 | 7 | 8 | 9 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= 10 | 11 | 12 | 13 | 14 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/ui/adapter/LocationRVAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.ui.adapter 2 | 3 | import android.app.Activity 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageButton 8 | import android.widget.TextView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import kotlinx.coroutines.CoroutineExceptionHandler 11 | import me.tangobee.weathernaut.R 12 | import me.tangobee.weathernaut.models.GeoWeatherModel 13 | import me.tangobee.weathernaut.models.GeocodingData.GeocodingResult 14 | import me.tangobee.weathernaut.utils.GeocodingHelper 15 | import me.tangobee.weathernaut.utils.SharedPreferencesHelper 16 | import me.tangobee.weathernaut.viewmodels.WeatherViewModel 17 | 18 | class LocationRVAdapter(private val locationRVModalList: ArrayList, private val lat: Double, private val long: Double, private val coroutineExceptionHandler: CoroutineExceptionHandler, private val weatherViewModel: WeatherViewModel) : RecyclerView.Adapter() { 19 | 20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 21 | return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.cities_item_layout, parent, false)) 22 | } 23 | 24 | override fun getItemCount(): Int { 25 | return locationRVModalList.size 26 | } 27 | 28 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 29 | holder.cityName.text = locationRVModalList[position].name 30 | val cityAddress = listOf( 31 | locationRVModalList[position].admin4, 32 | locationRVModalList[position].admin3, 33 | locationRVModalList[position].admin2, 34 | locationRVModalList[position].admin1, 35 | locationRVModalList[position].country 36 | ).filterNot { it.isNullOrBlank() }.joinToString(", ") 37 | 38 | holder.cityAddress.text = cityAddress 39 | 40 | holder.addRemoveCity.setOnClickListener { 41 | holder.addRemoveCity.setImageResource(R.drawable.icon_right_arrow) 42 | val context = holder.itemView.context 43 | 44 | val geoWeatherModel = GeoWeatherModel( 45 | listOf( 46 | locationRVModalList[position].latitude, 47 | locationRVModalList[position].longitude 48 | ), 49 | locationRVModalList[position].name, 50 | locationRVModalList[position].country, 51 | locationRVModalList[position].timezone 52 | ) 53 | 54 | weatherViewModel.getGeoWeather(coroutineExceptionHandler, geoWeatherModel) 55 | 56 | val sharedPreferencesHelper = SharedPreferencesHelper(context) 57 | sharedPreferencesHelper.saveGeocodingData(geoWeatherModel) 58 | 59 | if (context is Activity) { 60 | context.onBackPressed() 61 | } 62 | } 63 | 64 | val samePlace = GeocodingHelper.areLocationsSame(lat, long, locationRVModalList[position].latitude, locationRVModalList[position].longitude) 65 | 66 | if(samePlace) { 67 | holder.addRemoveCity.setImageResource(R.drawable.icon_right_arrow) 68 | } else { 69 | holder.addRemoveCity.setImageResource(R.drawable.icon_add) 70 | } 71 | } 72 | 73 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 74 | val cityName: TextView = itemView.findViewById(R.id.cityName) 75 | val cityAddress: TextView = itemView.findViewById(R.id.cityAddress) 76 | val addRemoveCity: ImageButton = itemView.findViewById(R.id.addRemoveCity) 77 | } 78 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Weathernaut 2 | 3 | [![Releases](https://img.shields.io/github/v/release/tangobeee/Weathernaut.svg)](https://github.com/tangobeee/Weathernaut/releases/latest) 4 | ![GitHub](https://img.shields.io/github/license/tangobeee/weathernaut) 5 | ![Release version on IzzyOnDroid](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/me.tangobee.weathernaut) 6 | 7 |

8 | 9 | **Requirements** 10 | - Android Studio latest version 11 | - JDK 8 12 | - Target Android SDK 35 13 | - Supports API Level +24 14 | - Material Components 1.10.0 15 | 16 | **Highlights** 17 | - Use [Open-Meteo] API 18 | - Use Material Design 2 19 | - Responsive UI 20 | - Background Lofi Music 21 | - Uses locale Database 22 | 23 | [Download from IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/me.tangobee.weathernaut/) 26 | [Download from Google Play](https://play.google.com/store/apps/details?id=me.tangobee.weathernaut) 29 | [Direct apk download](https://github.com/tangobeee/Weathernaut/releases/latest) 32 | 33 | **Screenshot** 34 | 35 |
36 | 37 |
38 | 39 | 40 | 41 | **Libraries & Dependencies** 42 | - [Support libraries]: appcompat / recyclerview / constraintlayout / splashscreen / Navigation Component 43 | - [Material Design 2]: MaterialCardView / MaterialButton 44 | - Square [Retrofit] / [GSON] 45 | - Custom Views: [Dots Indicator] / [Switcher] 46 | - Intuit: [SSP] / [SDP] 47 | 48 | **Credits** 49 | 50 | # Click here -> [credit-link] 51 | 52 | 53 | **Backend** 54 | 55 | # Click here -> [background-music] 56 | 57 | # License 58 | MIT License 59 | 60 | Copyright (c) 2023 TangoBee 61 | 62 | Permission is hereby granted, free of charge, to any person obtaining a copy 63 | of this software and associated documentation files (the "Software"), to deal 64 | in the Software without restriction, including without limitation the rights 65 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 66 | copies of the Software, and to permit persons to whom the Software is 67 | furnished to do so, subject to the following conditions: 68 | 69 | The above copyright notice and this permission notice shall be included in all 70 | copies or substantial portions of the Software. 71 | 72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 73 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 74 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 75 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 76 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 77 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 78 | SOFTWARE. 79 | 80 | [Open-Meteo]: https://open-meteo.com/ 81 | [credit-link]: https://tangobee.netlify.app/weathernaut/credits 82 | [Support libraries]: https://developer.android.com/jetpack/androidx/ 83 | [Material Design 2]: https://material.io/develop/android/ 84 | [Retrofit]: https://github.com/square/retrofit 85 | [GSON]: https://github.com/square/retrofit/tree/master/retrofit-converters/gson 86 | [Dots Indicator]: https://github.com/tommybuonomo/dotsindicator 87 | [SDP]: https://github.com/intuit/sdp 88 | [SSP]: https://github.com/intuit/ssp 89 | [Switcher]: https://github.com/bitvale/Switcher 90 | [background-music]: https://github.com/tangobeee/Weathernaut-Backend 91 | [Release]: https://github.com/tangobeee/Weathernaut/releases -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/utils/WeatherHelper.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.utils 2 | 3 | import me.tangobee.weathernaut.constants.UnitsMapper 4 | import me.tangobee.weathernaut.models.SettingsModel 5 | import me.tangobee.weathernaut.models.WeatherData.WeatherData 6 | 7 | class WeatherHelper(private val settings: SettingsModel, private val weatherData: WeatherData) { 8 | 9 | private val unitConverter = UnitConverter() 10 | 11 | fun convertWeatherData(): WeatherData { 12 | weatherData.current_weather.current.temperature_2m = convertTemperature( 13 | weatherData.current_weather.current.temperature_2m, 14 | weatherData.current_weather.current_units.temperature_2m, 15 | settings.tempUnit 16 | ) 17 | weatherData.current_weather.current_units.temperature_2m = UnitsMapper.getShorthandUnit(settings.tempUnit) 18 | 19 | weatherData.current_weather.current.wind_speed_10m = convertWindSpeed( 20 | weatherData.current_weather.current.wind_speed_10m, 21 | weatherData.current_weather.current_units.wind_speed_10m, 22 | settings.windSpeedUnit 23 | ) 24 | weatherData.current_weather.current_units.wind_speed_10m = UnitsMapper.getShorthandUnit(settings.windSpeedUnit) 25 | 26 | weatherData.current_weather.current.pressure_msl = convertPressure( 27 | weatherData.current_weather.current.pressure_msl, 28 | weatherData.current_weather.current_units.pressure_msl, 29 | settings.pressureUnit 30 | ) 31 | weatherData.current_weather.current_units.pressure_msl = UnitsMapper.getShorthandUnit(settings.pressureUnit) 32 | 33 | weatherData.hourly_weather.hourly.temperature_2m = weatherData.hourly_weather.hourly.temperature_2m.map { 34 | convertTemperature(it, weatherData.hourly_weather.hourly_units.temperature_2m, settings.tempUnit) 35 | } 36 | weatherData.hourly_weather.hourly_units.temperature_2m = UnitsMapper.getShorthandUnit(settings.tempUnit) 37 | 38 | weatherData.daily_weather.daily.temperature_2m_max = weatherData.daily_weather.daily.temperature_2m_max.map { 39 | convertTemperature(it, weatherData.daily_weather.daily_units.temperature_2m_max, settings.tempUnit) 40 | } 41 | weatherData.daily_weather.daily_units.temperature_2m_max = UnitsMapper.getShorthandUnit(settings.tempUnit) 42 | 43 | weatherData.daily_weather.daily.temperature_2m_min = weatherData.daily_weather.daily.temperature_2m_min.map { 44 | convertTemperature(it, weatherData.daily_weather.daily_units.temperature_2m_min, settings.tempUnit) 45 | } 46 | weatherData.daily_weather.daily_units.temperature_2m_min = UnitsMapper.getShorthandUnit(settings.tempUnit) 47 | 48 | return weatherData 49 | } 50 | 51 | private fun convertTemperature(value: Double, fromUnit: String, toUnit: String): Double { 52 | val fromUnitName = unitName(fromUnit) 53 | val toUnitName = unitName(toUnit) 54 | return unitConverter.convertTemperature(value, fromUnitName, toUnitName) 55 | } 56 | 57 | private fun convertWindSpeed(value: Double, fromUnit: String, toUnit: String): Double { 58 | val fromUnitName = unitName(fromUnit) 59 | val toUnitName = unitName(toUnit) 60 | return unitConverter.convertWindSpeed(value, fromUnitName, toUnitName) 61 | } 62 | 63 | private fun convertPressure(value: Double, fromUnit: String, toUnit: String): Double { 64 | val fromUnitName = unitName(fromUnit) 65 | val toUnitName = unitName(toUnit) 66 | return unitConverter.convertPressure(value, fromUnitName, toUnitName) 67 | } 68 | 69 | private fun unitName(unit: String): String { 70 | return when (unit) { 71 | "°C" -> "celsius" 72 | "°F" -> "fahrenheit" 73 | "km/h" -> "kmh" 74 | "mph" -> "mph" 75 | "m/s" -> "ms" 76 | "hPa" -> "hpa" 77 | "atm" -> "atm" 78 | else -> unit 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 9 | import androidx.lifecycle.MutableLiveData 10 | import androidx.lifecycle.ViewModelProvider 11 | import androidx.lifecycle.viewModelScope 12 | import kotlinx.coroutines.CoroutineExceptionHandler 13 | import kotlinx.coroutines.cancel 14 | import me.tangobee.weathernaut.data.RetrofitHelper 15 | import me.tangobee.weathernaut.data.api.WeatherService 16 | import me.tangobee.weathernaut.data.repository.WeatherRepository 17 | import me.tangobee.weathernaut.databinding.ActivityMainBinding 18 | import me.tangobee.weathernaut.models.WeatherData.WeatherData 19 | import me.tangobee.weathernaut.services.WeatherMusicService 20 | import me.tangobee.weathernaut.utils.SharedPreferencesHelper 21 | import me.tangobee.weathernaut.utils.WeatherHelper 22 | import me.tangobee.weathernaut.viewmodels.WeatherViewModel 23 | import me.tangobee.weathernaut.viewmodels.WeatherViewModelFactory 24 | import java.net.UnknownHostException 25 | import kotlin.system.exitProcess 26 | 27 | class MainActivity : AppCompatActivity() { 28 | 29 | private lateinit var binding: ActivityMainBinding 30 | 31 | private lateinit var weatherViewModel: WeatherViewModel 32 | private lateinit var coroutineExceptionHandler: CoroutineExceptionHandler 33 | private lateinit var sharedPreferencesHelper: SharedPreferencesHelper 34 | 35 | private var settingsUpdated = false 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | val splashScreen = installSplashScreen() 40 | binding = ActivityMainBinding.inflate(layoutInflater) 41 | 42 | sharedPreferencesHelper = SharedPreferencesHelper(this) 43 | 44 | val noInternetLiveData : MutableLiveData = MutableLiveData(false) 45 | 46 | coroutineExceptionHandler = CoroutineExceptionHandler{_, throwable -> 47 | if(throwable is UnknownHostException) { 48 | noInternetLiveData.postValue(true) 49 | } 50 | } 51 | 52 | noInternetLiveData.observe(this) {noInternet -> 53 | if(noInternet) { 54 | Toast.makeText(this, getString(R.string.no_internet), Toast.LENGTH_LONG).show() 55 | Thread.sleep(1000) 56 | exitProcess(0) 57 | } 58 | } 59 | 60 | val settingsModel = SharedPreferencesHelper(this).getSettings() 61 | if(settingsModel?.isMusicOn != false) { 62 | val startMusicIntent = Intent(this, WeatherMusicService::class.java) 63 | startService(startMusicIntent) 64 | } 65 | 66 | fetchData() 67 | splashScreen.setKeepOnScreenCondition { (weatherViewModel.weatherLiveData.value == null) } 68 | 69 | setContentView(binding.root) 70 | } 71 | 72 | private fun fetchData() { 73 | val weatherService = RetrofitHelper.getInstance().create(WeatherService::class.java) 74 | val weatherRepository = WeatherRepository(weatherService) 75 | weatherViewModel = ViewModelProvider(this, WeatherViewModelFactory(weatherRepository))[WeatherViewModel::class.java] 76 | 77 | val geocodingData = sharedPreferencesHelper.getGeocodingData() 78 | 79 | if(weatherViewModel.weatherLiveData.value == null) { 80 | if(geocodingData == null) { 81 | weatherViewModel.getWeather(coroutineExceptionHandler) 82 | } else { 83 | weatherViewModel.getGeoWeather(coroutineExceptionHandler, geocodingData) 84 | } 85 | } 86 | 87 | weatherViewModel.weatherLiveData.observe(this) { weatherData -> 88 | if(weatherData == null) { 89 | Toast.makeText(this, getString(R.string.api_fetching_error), Toast.LENGTH_SHORT).show() 90 | Thread.sleep(1000) 91 | exitProcess(0) 92 | } else { 93 | if(!settingsUpdated) { 94 | createLocalDB(weatherData) 95 | } 96 | } 97 | } 98 | } 99 | 100 | private fun createLocalDB(weatherData: WeatherData) { 101 | val currentSettings = sharedPreferencesHelper.getSettings() 102 | if (currentSettings != null) { 103 | val weatherHelper = WeatherHelper(currentSettings, weatherData) 104 | val newWeatherData = weatherHelper.convertWeatherData() 105 | 106 | if(newWeatherData != weatherData) { 107 | settingsUpdated = true 108 | weatherViewModel.updateWeatherData(newWeatherData) 109 | } 110 | } 111 | } 112 | 113 | override fun onDestroy() { 114 | super.onDestroy() 115 | weatherViewModel.viewModelScope.cancel("ActivityDestroying") 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_sun_cloud_rain.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 45 | 46 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 79 | 80 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_sun_rain_cloud.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 45 | 46 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 79 | 80 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/ui/Next7DaysFragment.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.ui 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.activityViewModels 9 | import androidx.navigation.Navigation 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import androidx.recyclerview.widget.RecyclerView 12 | import me.tangobee.weathernaut.constants.WeatherImageMapper 13 | import me.tangobee.weathernaut.databinding.FragmentNext7DaysBinding 14 | import me.tangobee.weathernaut.models.DailyWeatherRVModel 15 | import me.tangobee.weathernaut.models.WeatherData.WeatherData 16 | import me.tangobee.weathernaut.ui.adapter.DailyWeatherRVAdapter 17 | import me.tangobee.weathernaut.viewmodels.WeatherViewModel 18 | import java.text.SimpleDateFormat 19 | import java.util.Calendar 20 | import java.util.Locale 21 | import java.util.TimeZone 22 | 23 | class Next7DaysFragment : Fragment() { 24 | 25 | private lateinit var binding: FragmentNext7DaysBinding 26 | 27 | private val weatherViewModel: WeatherViewModel by activityViewModels() 28 | private lateinit var dailyWeatherRVModelList: ArrayList 29 | 30 | override fun onCreateView( 31 | inflater: LayoutInflater, container: ViewGroup?, 32 | savedInstanceState: Bundle? 33 | ): View { 34 | // Inflate the layout for this fragment 35 | binding = FragmentNext7DaysBinding.inflate(inflater, container, false) 36 | return binding.root 37 | } 38 | 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 40 | super.onViewCreated(view, savedInstanceState) 41 | 42 | weatherViewModel.weatherLiveData.observe(viewLifecycleOwner) {weatherData -> 43 | if(weatherData != null) { 44 | setDailyWeatherRecyclerViewUIData(weatherData) 45 | } 46 | } 47 | 48 | binding.backButton.setOnClickListener { 49 | Navigation.findNavController(requireView()).popBackStack() 50 | } 51 | } 52 | 53 | private fun setDailyWeatherRecyclerViewUIData(weatherData: WeatherData) { 54 | val tomorrowWeatherTemp = (weatherData.daily_weather.daily.temperature_2m_min[1].toInt().toString() + "/" + weatherData.daily_weather.daily.temperature_2m_max[1].toInt().toString()) 55 | binding.tomorrowWeatherTemperature.text = tomorrowWeatherTemp 56 | binding.sunriseTime.text = convertDateStringToFormattedTime(weatherData.daily_weather.daily.sunrise[1]) 57 | binding.sunsetTime.text = convertDateStringToFormattedTime(weatherData.daily_weather.daily.sunset[1]) 58 | binding.tomorrowWeatherIcon.setImageResource(WeatherImageMapper.getImageForWeatherCode(weatherData.daily_weather.daily.weather_code[1])) 59 | 60 | dailyWeatherRVModelList = ArrayList() 61 | for(i in 2..7) { 62 | val day = getDayOfWeek(weatherData.daily_weather.daily.time[i]) 63 | val weatherTemp = (weatherData.daily_weather.daily.temperature_2m_min[i].toInt().toString() + "/" + weatherData.daily_weather.daily.temperature_2m_max[i].toInt().toString()) 64 | val weatherIcon = WeatherImageMapper.getImageForWeatherCode(weatherData.daily_weather.daily.weather_code[i]) 65 | dailyWeatherRVModelList.add(DailyWeatherRVModel(day, weatherTemp, weatherIcon)) 66 | } 67 | 68 | val next7DaysWeatherRVLinearLayoutManager = object : LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) { 69 | override fun canScrollVertically(): Boolean { 70 | return false 71 | } 72 | } 73 | binding.next7DaysWeatherRV.setHasFixedSize(true) 74 | binding.next7DaysWeatherRV.layoutManager = next7DaysWeatherRVLinearLayoutManager 75 | val next7DaysRVAdapter = DailyWeatherRVAdapter(dailyWeatherRVModelList) 76 | binding.next7DaysWeatherRV.adapter = next7DaysRVAdapter 77 | } 78 | 79 | private fun convertDateStringToFormattedTime(dateStr: String): String { 80 | val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault()) 81 | val outputFormat = SimpleDateFormat("hh:mm a", Locale.getDefault()) 82 | inputFormat.timeZone = TimeZone.getDefault() 83 | 84 | val date = inputFormat.parse(dateStr) 85 | val calendar = Calendar.getInstance() 86 | if (date != null) { 87 | calendar.time = date 88 | } 89 | 90 | return outputFormat.format(calendar.time) 91 | } 92 | 93 | private fun getDayOfWeek(dateStr: String): String { 94 | val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) 95 | val date = format.parse(dateStr) 96 | val calendar = Calendar.getInstance() 97 | if (date != null) { 98 | calendar.time = date 99 | } 100 | 101 | val days = arrayOf("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") 102 | return days[calendar.get(Calendar.DAY_OF_WEEK) - 1] 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_pressure.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 14 | 16 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 49 | 52 | 55 | 58 | 61 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_next_7_days.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 32 | 33 | 41 | 42 | 53 | 54 | 65 | 66 | 74 | 75 | 85 | 86 | 96 | 97 | 107 | 108 | 118 | 119 | 120 | 128 | 129 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_thunderstorm_cloud.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 10 | 13 | 19 | 22 | 28 | 31 | 37 | 40 | 43 | 44 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/ui/SearchFragment.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.ui 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.ScrollView 9 | import android.widget.Toast 10 | import androidx.appcompat.widget.SearchView.OnQueryTextListener 11 | import androidx.core.content.ContextCompat 12 | import androidx.fragment.app.activityViewModels 13 | import androidx.lifecycle.MutableLiveData 14 | import androidx.lifecycle.ViewModelProvider 15 | import androidx.recyclerview.widget.LinearLayoutManager 16 | import androidx.recyclerview.widget.RecyclerView 17 | import kotlinx.coroutines.CoroutineExceptionHandler 18 | import me.tangobee.weathernaut.R 19 | import me.tangobee.weathernaut.data.RetrofitHelper 20 | import me.tangobee.weathernaut.data.api.GeocodingService 21 | import me.tangobee.weathernaut.data.repository.GeocodingRepository 22 | import me.tangobee.weathernaut.databinding.FragmentSearchBinding 23 | import me.tangobee.weathernaut.models.GeocodingData.GeocodingModel 24 | import me.tangobee.weathernaut.models.GeocodingData.GeocodingResult 25 | import me.tangobee.weathernaut.models.NewLocationModel 26 | import me.tangobee.weathernaut.ui.adapter.LocationRVAdapter 27 | import me.tangobee.weathernaut.utils.GeocodingHelper 28 | import me.tangobee.weathernaut.viewmodels.GeocodingViewModel 29 | import me.tangobee.weathernaut.viewmodels.GeocodingViewModelFactory 30 | import me.tangobee.weathernaut.viewmodels.WeatherViewModel 31 | import java.net.UnknownHostException 32 | import kotlin.system.exitProcess 33 | 34 | class SearchFragment : Fragment() { 35 | 36 | private lateinit var binding: FragmentSearchBinding 37 | 38 | private val weatherViewModel: WeatherViewModel by activityViewModels() 39 | private var lat: Double = 0.0 40 | private var long: Double = 0.0 41 | private lateinit var geocodingViewModel: GeocodingViewModel 42 | private lateinit var coroutineExceptionHandler: CoroutineExceptionHandler 43 | private lateinit var mainContainerView: ScrollView 44 | 45 | private var locationRVModelList: ArrayList = ArrayList() 46 | 47 | override fun onCreateView( 48 | inflater: LayoutInflater, container: ViewGroup?, 49 | savedInstanceState: Bundle? 50 | ): View { 51 | binding = FragmentSearchBinding.inflate(inflater, container, false) 52 | return binding.root 53 | } 54 | 55 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 56 | super.onViewCreated(view, savedInstanceState) 57 | mainContainerView = requireActivity().findViewById(R.id.mainContainerView) 58 | mainContainerView.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.white)) 59 | 60 | val geocodingService = RetrofitHelper.getInstance().create(GeocodingService::class.java) 61 | val geocodingRepository = GeocodingRepository(geocodingService) 62 | geocodingViewModel = ViewModelProvider(requireActivity(), GeocodingViewModelFactory(geocodingRepository))[GeocodingViewModel::class.java] 63 | 64 | coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> 65 | if (throwable is UnknownHostException) { 66 | Toast.makeText(requireContext(), getString(R.string.no_internet), Toast.LENGTH_LONG).show() 67 | exitProcess(0) 68 | } 69 | } 70 | 71 | weatherViewModel.weatherLiveData.observe(viewLifecycleOwner) { weatherData -> 72 | if(weatherData != null) { 73 | lat = weatherData.lat 74 | long = weatherData.long 75 | } 76 | } 77 | 78 | binding.cancelSearch.setOnClickListener { 79 | requireActivity().onBackPressedDispatcher.onBackPressed() 80 | } 81 | 82 | binding.searchCity.setOnQueryTextListener(object: OnQueryTextListener { 83 | override fun onQueryTextSubmit(newLocationQuery: String?): Boolean { 84 | if (newLocationQuery.isNullOrEmpty()) { 85 | showPlaceholder(getString(R.string.search_your_city)) 86 | } else if (newLocationQuery.length <= 2) { 87 | showPlaceholder(getString(R.string.no_locations_found)) 88 | } else { 89 | fetchLocation(newLocationQuery) 90 | } 91 | return true 92 | } 93 | 94 | override fun onQueryTextChange(locationQuery: String?): Boolean { 95 | if (locationQuery.isNullOrEmpty()) { 96 | showPlaceholder(getString(R.string.search_your_city)) 97 | } 98 | return true 99 | } 100 | }) 101 | 102 | observeViewModel() 103 | } 104 | 105 | private fun fetchLocation(newLocationQuery: String) { 106 | showPlaceholder(getString(R.string.loading)) 107 | val newLocationDataModel = NewLocationModel(newLocationQuery) 108 | geocodingViewModel.getLocations(newLocationDataModel, coroutineExceptionHandler) 109 | } 110 | 111 | private fun observeViewModel() { 112 | geocodingViewModel.locationLiveData.observe(viewLifecycleOwner) { locationData -> 113 | if (locationData == null) { 114 | showPlaceholder(getString(R.string.something_went_wrong)) 115 | Toast.makeText(requireContext(), getString(R.string.api_fetching_error), Toast.LENGTH_SHORT).show() 116 | } else if (locationData.results.isNullOrEmpty()) { 117 | showPlaceholder(getString(R.string.no_locations_found)) 118 | } else { 119 | setLocationRecyclerViewUIData(locationData) 120 | } 121 | } 122 | } 123 | 124 | private fun setLocationRecyclerViewUIData(locationData: GeocodingModel) { 125 | locationRVModelList = GeocodingHelper.sortLocationsByProximity( 126 | lat, 127 | long, 128 | locationData.results ?: ArrayList() 129 | ).toCollection(ArrayList()) 130 | 131 | if (locationRVModelList.isEmpty()) { 132 | showPlaceholder(getString(R.string.no_locations_found)) 133 | } else { 134 | binding.citiesRecyclerView.visibility = View.VISIBLE 135 | binding.searchPlaceholderTV.visibility = View.GONE 136 | 137 | val locationRVLinearLayoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) 138 | binding.citiesRecyclerView.setHasFixedSize(true) 139 | binding.citiesRecyclerView.layoutManager = locationRVLinearLayoutManager 140 | val locationAdapter = LocationRVAdapter(locationRVModelList, lat, long, coroutineExceptionHandler, weatherViewModel) 141 | binding.citiesRecyclerView.adapter = locationAdapter 142 | } 143 | } 144 | 145 | private fun showPlaceholder(message: String) { 146 | binding.searchPlaceholderTV.text = message 147 | binding.citiesRecyclerView.visibility = View.GONE 148 | binding.searchPlaceholderTV.visibility = View.VISIBLE 149 | } 150 | 151 | override fun onResume() { 152 | super.onResume() 153 | showPlaceholder(getString(R.string.search_your_city)) 154 | } 155 | 156 | override fun onDestroy() { 157 | super.onDestroy() 158 | mainContainerView.background = ContextCompat.getDrawable(requireContext(), R.drawable.home_background) 159 | if(locationRVModelList.isNotEmpty()) { 160 | locationRVModelList.clear() 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/ui/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.ui 2 | 3 | import android.os.Bundle 4 | import android.text.SpannableString 5 | import android.text.style.RelativeSizeSpan 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.constraintlayout.widget.ConstraintSet 10 | import androidx.core.content.ContextCompat 11 | import androidx.core.content.res.ResourcesCompat 12 | import androidx.fragment.app.Fragment 13 | import androidx.fragment.app.activityViewModels 14 | import androidx.navigation.Navigation 15 | import androidx.recyclerview.widget.LinearLayoutManager 16 | import androidx.recyclerview.widget.RecyclerView 17 | import me.tangobee.weathernaut.R 18 | import me.tangobee.weathernaut.constants.WeatherCodes 19 | import me.tangobee.weathernaut.constants.WeatherImageMapper 20 | import me.tangobee.weathernaut.databinding.FragmentHomeBinding 21 | import me.tangobee.weathernaut.models.HourlyWeatherRVModel 22 | import me.tangobee.weathernaut.models.WeatherData.WeatherData 23 | import me.tangobee.weathernaut.ui.adapter.HourlyWeatherRVAdapter 24 | import me.tangobee.weathernaut.viewmodels.WeatherViewModel 25 | import java.text.SimpleDateFormat 26 | import java.util.Calendar 27 | import java.util.Locale 28 | import java.util.TimeZone 29 | 30 | 31 | class HomeFragment : Fragment() { 32 | 33 | private lateinit var binding: FragmentHomeBinding 34 | 35 | private val weatherViewModel: WeatherViewModel by activityViewModels() 36 | private lateinit var todayHourlyWeatherRVAdapter: HourlyWeatherRVAdapter 37 | private lateinit var tomorrowHourlyWeatherRVAdapter: HourlyWeatherRVAdapter 38 | private val todayHourlyWeatherRVModelList: ArrayList = ArrayList() 39 | private val tomorrowHourlyWeatherRVModelList: ArrayList = ArrayList() 40 | 41 | override fun onCreateView( 42 | inflater: LayoutInflater, container: ViewGroup?, 43 | savedInstanceState: Bundle? 44 | ): View { 45 | // Inflate the layout for this fragment 46 | binding = FragmentHomeBinding.inflate(inflater, container, false) 47 | return binding.root 48 | } 49 | 50 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 51 | super.onViewCreated(view, savedInstanceState) 52 | 53 | binding.next7DaysIndicator.setOnClickListener { 54 | Navigation.findNavController(requireView()).navigate(R.id.action_homeFragment_to_next7DaysFragment) 55 | } 56 | 57 | binding.settings.setOnClickListener { 58 | Navigation.findNavController(requireView()).navigate(R.id.action_homeFragment_to_settingsFragment) 59 | } 60 | 61 | binding.searchCities.setOnClickListener { 62 | Navigation.findNavController(requireView()).navigate(R.id.action_homeFragment_to_searchFragment) 63 | } 64 | 65 | binding.todayIndicator.setOnClickListener { 66 | changeIndicatorDotPosition(R.id.todayIndicator) 67 | binding.hourlyWeatherRV.swapAdapter(todayHourlyWeatherRVAdapter, false) 68 | 69 | binding.todayIndicator.setTextColor(ContextCompat.getColor(requireContext(), R.color.textColor)) 70 | binding.tomorrowIndicator.setTextColor(ContextCompat.getColor(requireContext(), R.color.unselectedIndicatorColor)) 71 | 72 | val boldTypeface = ResourcesCompat.getFont(requireContext(), R.font.inter_bold) 73 | val typeface = ResourcesCompat.getFont(requireContext(), R.font.inter) 74 | binding.todayIndicator.typeface = boldTypeface 75 | binding.tomorrowIndicator.typeface = typeface 76 | 77 | binding.todayIndicator.isEnabled = false 78 | binding.tomorrowIndicator.isEnabled = true 79 | } 80 | 81 | binding.tomorrowIndicator.setOnClickListener { 82 | changeIndicatorDotPosition(R.id.tomorrowIndicator) 83 | binding.hourlyWeatherRV.swapAdapter(tomorrowHourlyWeatherRVAdapter, false) 84 | 85 | binding.tomorrowIndicator.setTextColor(ContextCompat.getColor(requireContext(), R.color.textColor)) 86 | binding.todayIndicator.setTextColor(ContextCompat.getColor(requireContext(), R.color.unselectedIndicatorColor)) 87 | 88 | val boldTypeface = ResourcesCompat.getFont(requireContext(), R.font.inter_bold) 89 | val typeface = ResourcesCompat.getFont(requireContext(), R.font.inter) 90 | binding.tomorrowIndicator.typeface = boldTypeface 91 | binding.todayIndicator.typeface = typeface 92 | 93 | binding.tomorrowIndicator.isEnabled = false 94 | binding.todayIndicator.isEnabled = true 95 | } 96 | 97 | 98 | weatherViewModel.weatherLiveData.observe(viewLifecycleOwner) {weatherData -> 99 | if(weatherData != null) { 100 | setCurrentWeatherUIData(weatherData) 101 | setHourlyRecyclerViewUIData(weatherData) 102 | } 103 | } 104 | } 105 | 106 | private fun setCurrentWeatherUIData(weatherData: WeatherData) { 107 | val city = "${weatherData.city}\n" 108 | val country = weatherData.country 109 | val region = SpannableString("$city$country") 110 | region.setSpan( 111 | RelativeSizeSpan(0.7f), 112 | city.length, 113 | region.length, 114 | SpannableString.SPAN_INCLUSIVE_EXCLUSIVE 115 | ) 116 | binding.locationName.text = region 117 | 118 | binding.date.text = convertISO8601ToCustomDateFormat(weatherData.current_weather.current.time) 119 | binding.currentWeatherTemperature.text = weatherData.current_weather.current.temperature_2m.toInt().toString() 120 | binding.weatherUnit.text = weatherData.current_weather.current_units.temperature_2m 121 | binding.weatherIcon.setImageResource(WeatherImageMapper.getImageForWeatherCode(weatherData.current_weather.current.weather_code)) 122 | binding.currentWeatherType.text = WeatherCodes.weatherConstants.first {it.code == weatherData.current_weather.current.weather_code}.description 123 | 124 | val currentPressure = weatherData.current_weather.current.pressure_msl.toString() + weatherData.current_weather.current_units.pressure_msl 125 | binding.pressureValue.text = currentPressure 126 | 127 | val windPressure = weatherData.current_weather.current.wind_speed_10m.toString() + weatherData.current_weather.current_units.wind_speed_10m 128 | binding.windValue.text = windPressure 129 | 130 | val humidityPressure = weatherData.current_weather.current.relative_humidity_2m.toString() + weatherData.current_weather.current_units.relative_humidity_2m 131 | binding.humidityValue.text = humidityPressure 132 | } 133 | private fun setHourlyRecyclerViewUIData(weatherData: WeatherData) { 134 | var currentHourlyWeatherItemPosition = 0 135 | todayHourlyWeatherRVModelList.clear() 136 | for(i in 0 .. 23) { 137 | var time = convertDateStringToFormattedTime(weatherData.hourly_weather.hourly.time[i]) 138 | if(isCurrentLocalTime(time)) { 139 | time = "now" 140 | currentHourlyWeatherItemPosition = i 141 | } 142 | 143 | val weatherIcon = WeatherImageMapper.getImageForWeatherCode(weatherData.hourly_weather.hourly.weather_code[i]) 144 | val weatherTemp = weatherData.hourly_weather.hourly.temperature_2m[i] 145 | 146 | todayHourlyWeatherRVModelList.add(HourlyWeatherRVModel(time, weatherIcon, "${weatherTemp.toInt()}°")) 147 | } 148 | 149 | tomorrowHourlyWeatherRVModelList.clear() 150 | for(i in 24 .. 47) { 151 | val time = convertDateStringToFormattedTime(weatherData.hourly_weather.hourly.time[i]) 152 | val weatherIcon = WeatherImageMapper.getImageForWeatherCode(weatherData.hourly_weather.hourly.weather_code[i]) 153 | val weatherTemp = weatherData.hourly_weather.hourly.temperature_2m[i] 154 | 155 | tomorrowHourlyWeatherRVModelList.add(HourlyWeatherRVModel(time, weatherIcon, "${weatherTemp.toInt()}°")) 156 | } 157 | 158 | binding.hourlyWeatherRV.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) 159 | todayHourlyWeatherRVAdapter = HourlyWeatherRVAdapter(todayHourlyWeatherRVModelList) 160 | tomorrowHourlyWeatherRVAdapter = HourlyWeatherRVAdapter(tomorrowHourlyWeatherRVModelList) 161 | binding.hourlyWeatherRV.adapter = todayHourlyWeatherRVAdapter 162 | 163 | binding.hourlyWeatherRV.scrollToPosition(currentHourlyWeatherItemPosition) 164 | 165 | binding.tomorrowIndicator.isEnabled = true 166 | binding.todayIndicator.isEnabled = true 167 | } 168 | 169 | private fun convertISO8601ToCustomDateFormat(iso8601Date: String): String? { 170 | val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault()) 171 | val outputFormat = SimpleDateFormat("EEE, dd MMM yyyy", Locale.getDefault()) 172 | 173 | val date = inputFormat.parse(iso8601Date) 174 | return date?.let { outputFormat.format(it) } 175 | } 176 | private fun convertDateStringToFormattedTime(dateStr: String): String { 177 | val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault()) 178 | val outputFormat = SimpleDateFormat("hh:mm a", Locale.getDefault()) 179 | inputFormat.timeZone = TimeZone.getDefault() 180 | 181 | val date = inputFormat.parse(dateStr) 182 | val calendar = Calendar.getInstance() 183 | if (date != null) { 184 | calendar.time = date 185 | } 186 | 187 | return outputFormat.format(calendar.time) 188 | } 189 | 190 | private fun changeIndicatorDotPosition(viewId: Int) { 191 | val constraintSet = ConstraintSet() 192 | constraintSet.clone(binding.homeFragmentLayout) 193 | constraintSet.connect(R.id.indicator, ConstraintSet.START, viewId, ConstraintSet.START, 0) 194 | constraintSet.connect(R.id.indicator, ConstraintSet.END, viewId, ConstraintSet.END, 0) 195 | constraintSet.applyTo(binding.homeFragmentLayout) 196 | } 197 | 198 | private fun isCurrentLocalTime(timeString: String): Boolean { 199 | val sdf = SimpleDateFormat("hh:mm a", Locale.getDefault()) 200 | val currentTime = Calendar.getInstance() 201 | val parsedTime = sdf.parse(timeString) 202 | 203 | if (parsedTime != null) { 204 | val parsedCalendar = Calendar.getInstance() 205 | parsedCalendar.time = parsedTime 206 | 207 | return currentTime.get(Calendar.HOUR_OF_DAY) == parsedCalendar.get(Calendar.HOUR_OF_DAY) 208 | } 209 | 210 | return false 211 | } 212 | } -------------------------------------------------------------------------------- /app/src/main/java/me/tangobee/weathernaut/ui/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package me.tangobee.weathernaut.ui 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import androidx.fragment.app.Fragment 7 | import android.view.LayoutInflater 8 | import android.view.MenuItem 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.ScrollView 12 | import android.widget.Toast 13 | import androidx.appcompat.widget.PopupMenu 14 | import androidx.core.content.ContextCompat 15 | import androidx.fragment.app.activityViewModels 16 | import me.tangobee.weathernaut.R 17 | import me.tangobee.weathernaut.constants.UnitsMapper 18 | import me.tangobee.weathernaut.databinding.FragmentSettingsBinding 19 | import me.tangobee.weathernaut.models.SettingsModel 20 | import me.tangobee.weathernaut.models.WeatherData.WeatherData 21 | import me.tangobee.weathernaut.services.WeatherMusicService 22 | import me.tangobee.weathernaut.utils.SharedPreferencesHelper 23 | import me.tangobee.weathernaut.utils.WeatherHelper 24 | import me.tangobee.weathernaut.viewmodels.WeatherViewModel 25 | 26 | class SettingsFragment : Fragment() { 27 | 28 | private lateinit var binding: FragmentSettingsBinding 29 | 30 | private val weatherViewModel: WeatherViewModel by activityViewModels() 31 | private lateinit var sharedPreferencesHelper: SharedPreferencesHelper 32 | private lateinit var settingsModel: SettingsModel 33 | 34 | private lateinit var weatherData: WeatherData 35 | 36 | private lateinit var mainContainerView: ScrollView 37 | 38 | override fun onCreateView( 39 | inflater: LayoutInflater, container: ViewGroup?, 40 | savedInstanceState: Bundle? 41 | ): View { 42 | // Inflate the layout for this fragment 43 | binding = FragmentSettingsBinding.inflate(inflater, container, false) 44 | return binding.root 45 | } 46 | 47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 48 | super.onViewCreated(view, savedInstanceState) 49 | mainContainerView = requireActivity().findViewById(R.id.mainContainerView) 50 | mainContainerView.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.white)) 51 | 52 | weatherViewModel.weatherLiveData.observe(viewLifecycleOwner) { weatherData -> 53 | if(weatherData != null) { 54 | this.weatherData = weatherData 55 | } 56 | } 57 | 58 | sharedPreferencesHelper = SharedPreferencesHelper(requireContext()) 59 | settingsModel = sharedPreferencesHelper.getSettings() ?: SettingsModel() 60 | sharedPreferencesHelper.saveSettings(settingsModel) 61 | setSettingsUIDate() 62 | 63 | binding.feedback.setOnClickListener { openURL(Uri.parse("mailto:hello.weathernaut@outlook.com?subject=Feedback on Weathernaut app.")) } 64 | binding.privacyPolicy.setOnClickListener { openURL(Uri.parse("https://tangobee.netlify.app/Weathernaut/privacy-policy")) } 65 | binding.donation.setOnClickListener { openURL(Uri.parse("https://tangobee.netlify.app/Weathernaut/donation")) } 66 | binding.credits.setOnClickListener { openURL(Uri.parse("https://tangobee.netlify.app/Weathernaut/credits")) } 67 | 68 | binding.temperatureUnit.setOnClickListener {showTemperatureUnitPopup(binding.temperatureSpinner)} 69 | binding.windSpeedUnit.setOnClickListener {showWindSpeedUnitPopup(binding.windSpeedSpinner)} 70 | binding.atmosphericPressureUnit.setOnClickListener {showAtmosphericPressureUnitPopup(binding.atmosphericPressureSpinner)} 71 | 72 | binding.backButton.setOnClickListener { 73 | requireActivity().onBackPressedDispatcher.onBackPressed() 74 | } 75 | 76 | binding.weatherMusicSwitch.setOnClickListener { 77 | val flag = binding.weatherMusicSwitch.isChecked 78 | binding.weatherMusicSwitch.setChecked(!flag) 79 | 80 | 81 | sharedPreferencesHelper.updateSettings { 82 | it.isMusicOn = !flag 83 | it 84 | } 85 | 86 | val currentSettings = sharedPreferencesHelper.getSettings() 87 | updateWeatherData(currentSettings) 88 | 89 | weatherMusicToggle(!flag) 90 | } 91 | 92 | binding.weatherMusicWrapper.setOnClickListener { 93 | val flag = binding.weatherMusicSwitch.isChecked 94 | binding.weatherMusicSwitch.setChecked(!flag) 95 | 96 | sharedPreferencesHelper.updateSettings { 97 | it.isMusicOn = !flag 98 | it 99 | } 100 | 101 | val currentSettings = sharedPreferencesHelper.getSettings() 102 | updateWeatherData(currentSettings) 103 | 104 | weatherMusicToggle(!flag) 105 | } 106 | } 107 | 108 | private fun setSettingsUIDate() { 109 | binding.temperatureSpinner.text = UnitsMapper.getShorthandUnit(settingsModel.tempUnit) 110 | binding.windSpeedSpinner.text = UnitsMapper.getShorthandUnit(settingsModel.windSpeedUnit) 111 | binding.atmosphericPressureSpinner.text = UnitsMapper.getShorthandUnit(settingsModel.pressureUnit) 112 | 113 | binding.weatherMusicSwitch.setChecked(settingsModel.isMusicOn) 114 | } 115 | 116 | private fun weatherMusicToggle(musicFlag: Boolean) { 117 | if(musicFlag) { 118 | Toast.makeText(requireContext(), "The music will start in a few seconds!", Toast.LENGTH_SHORT).show() 119 | val startMusicIntent = Intent(requireContext(), WeatherMusicService::class.java) 120 | requireActivity().startService(startMusicIntent) 121 | } 122 | else { 123 | val stopMusicIntent = Intent(requireContext(), WeatherMusicService::class.java) 124 | requireActivity().stopService(stopMusicIntent) 125 | } 126 | } 127 | 128 | private fun openURL(uri: Uri) { 129 | val intent = Intent(Intent.ACTION_VIEW, uri) 130 | startActivity(intent) 131 | } 132 | 133 | private fun showTemperatureUnitPopup(view: View) { 134 | val popup = PopupMenu(requireContext(), view) 135 | popup.inflate(R.menu.temperature_unit_menu) 136 | popup.setOnMenuItemClickListener { item: MenuItem? -> 137 | when (item!!.itemId) { 138 | R.id.celsius -> { 139 | binding.temperatureSpinner.text = item.title 140 | 141 | sharedPreferencesHelper.updateSettings { 142 | it.tempUnit = UnitsMapper.getFullUnit(binding.temperatureSpinner.text.toString()) 143 | it 144 | } 145 | 146 | val currentSettings = sharedPreferencesHelper.getSettings() 147 | updateWeatherData(currentSettings) 148 | } 149 | 150 | R.id.fahrenheit -> { 151 | binding.temperatureSpinner.text = item.title 152 | 153 | sharedPreferencesHelper.updateSettings { 154 | it.tempUnit = UnitsMapper.getFullUnit(binding.temperatureSpinner.text.toString()) 155 | it 156 | } 157 | 158 | val currentSettings = sharedPreferencesHelper.getSettings() 159 | updateWeatherData(currentSettings) 160 | } 161 | } 162 | 163 | true 164 | } 165 | 166 | popup.show() 167 | } 168 | private fun showWindSpeedUnitPopup(view: View) { 169 | val popup = PopupMenu(requireContext(), view) 170 | popup.inflate(R.menu.windsped_unit_menu) 171 | popup.setOnMenuItemClickListener { item: MenuItem? -> 172 | when (item!!.itemId) { 173 | R.id.kilometers -> { 174 | binding.windSpeedSpinner.text = item.title 175 | 176 | sharedPreferencesHelper.updateSettings { 177 | it.windSpeedUnit = UnitsMapper.getFullUnit(binding.windSpeedSpinner.text.toString()) 178 | it 179 | } 180 | 181 | val currentSettings = sharedPreferencesHelper.getSettings() 182 | updateWeatherData(currentSettings) 183 | } 184 | 185 | R.id.meters -> { 186 | binding.windSpeedSpinner.text = item.title 187 | 188 | sharedPreferencesHelper.updateSettings { 189 | it.windSpeedUnit = UnitsMapper.getFullUnit(binding.windSpeedSpinner.text.toString()) 190 | it 191 | } 192 | 193 | val currentSettings = sharedPreferencesHelper.getSettings() 194 | updateWeatherData(currentSettings) 195 | } 196 | 197 | R.id.miles -> { 198 | binding.windSpeedSpinner.text = item.title 199 | 200 | sharedPreferencesHelper.updateSettings { 201 | it.windSpeedUnit = UnitsMapper.getFullUnit(binding.windSpeedSpinner.text.toString()) 202 | it 203 | } 204 | 205 | val currentSettings = sharedPreferencesHelper.getSettings() 206 | updateWeatherData(currentSettings) 207 | } 208 | } 209 | 210 | true 211 | } 212 | 213 | popup.show() 214 | } 215 | private fun showAtmosphericPressureUnitPopup(view: View) { 216 | val popup = PopupMenu(requireContext(), view) 217 | popup.inflate(R.menu.atmospheric_pressure_unit_menu) 218 | popup.setOnMenuItemClickListener { item: MenuItem? -> 219 | when (item!!.itemId) { 220 | R.id.hpa -> { 221 | binding.atmosphericPressureSpinner.text = item.title 222 | 223 | sharedPreferencesHelper.updateSettings { 224 | it.pressureUnit = UnitsMapper.getFullUnit(binding.atmosphericPressureSpinner.text.toString()) 225 | it 226 | } 227 | 228 | val currentSettings = sharedPreferencesHelper.getSettings() 229 | updateWeatherData(currentSettings) 230 | } 231 | 232 | R.id.atm -> { 233 | binding.atmosphericPressureSpinner.text = item.title 234 | 235 | sharedPreferencesHelper.updateSettings { 236 | it.pressureUnit = UnitsMapper.getFullUnit(binding.atmosphericPressureSpinner.text.toString()) 237 | it 238 | } 239 | 240 | val currentSettings = sharedPreferencesHelper.getSettings() 241 | updateWeatherData(currentSettings) 242 | } 243 | } 244 | 245 | true 246 | } 247 | 248 | popup.show() 249 | } 250 | 251 | private fun updateWeatherData(currentSettings: SettingsModel?) { 252 | if(currentSettings != null) { 253 | val weatherHelper = WeatherHelper(currentSettings, weatherData) 254 | val newWeatherData = weatherHelper.convertWeatherData() 255 | 256 | weatherViewModel.updateWeatherData(newWeatherData) 257 | } 258 | } 259 | 260 | override fun onDestroy() { 261 | super.onDestroy() 262 | mainContainerView.background = ContextCompat.getDrawable(requireContext(), R.drawable.home_background) 263 | } 264 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_weather_snow_cloud.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 10 | 13 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 36 | 37 | --------------------------------------------------------------------------------