Start adding support for multiple locations
This commit is contained in:
parent
b8ab0605e8
commit
b32701b138
35 changed files with 377 additions and 231 deletions
|
@ -2,6 +2,7 @@ plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("kotlin-kapt")
|
id("kotlin-kapt")
|
||||||
|
kotlin("plugin.serialization") version "1.8.10"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -72,7 +73,7 @@ dependencies {
|
||||||
implementation("androidx.compose.material3:material3:1.1.0-rc01")
|
implementation("androidx.compose.material3:material3:1.1.0-rc01")
|
||||||
implementation("androidx.activity:activity-compose:1.7.1")
|
implementation("androidx.activity:activity-compose:1.7.1")
|
||||||
implementation("androidx.core:core-ktx:1.10.0")
|
implementation("androidx.core:core-ktx:1.10.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
|
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ dependencies {
|
||||||
// Retrofit
|
// Retrofit
|
||||||
val retrofitVersion = "2.9.0"
|
val retrofitVersion = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
||||||
implementation("com.squareup.retrofit2:converter-moshi:$retrofitVersion")
|
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
|
||||||
|
|
||||||
// Accompanist
|
// Accompanist
|
||||||
val accompanistVersion = "0.30.0"
|
val accompanistVersion = "0.30.0"
|
||||||
|
|
|
@ -5,13 +5,15 @@ import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import com.henryhiles.qweather.domain.remote.GeocodingApi
|
import com.henryhiles.qweather.domain.remote.GeocodingApi
|
||||||
import com.henryhiles.qweather.domain.remote.WeatherApi
|
import com.henryhiles.qweather.domain.remote.WeatherApi
|
||||||
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient.Builder
|
import okhttp3.OkHttpClient.Builder
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
|
||||||
import retrofit2.create
|
import retrofit2.create
|
||||||
|
|
||||||
private fun isNetworkAvailable(context: Context): Boolean {
|
private fun isNetworkAvailable(context: Context): Boolean {
|
||||||
|
@ -29,6 +31,9 @@ private fun isNetworkAvailable(context: Context): Boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val contentType = "application/json".toMediaType()
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
val appModule = module {
|
val appModule = module {
|
||||||
fun provideWeatherApi(context: Context): WeatherApi {
|
fun provideWeatherApi(context: Context): WeatherApi {
|
||||||
val cacheControlInterceptor = Interceptor { chain ->
|
val cacheControlInterceptor = Interceptor { chain ->
|
||||||
|
@ -56,7 +61,7 @@ val appModule = module {
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl("https://api.open-meteo.com")
|
.baseUrl("https://api.open-meteo.com")
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(MoshiConverterFactory.create())
|
.addConverterFactory(json.asConverterFactory(contentType))
|
||||||
.build()
|
.build()
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
@ -64,7 +69,7 @@ val appModule = module {
|
||||||
fun provideGeocodingApi(): GeocodingApi {
|
fun provideGeocodingApi(): GeocodingApi {
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl("https://geocoding-api.open-meteo.com")
|
.baseUrl("https://geocoding-api.open-meteo.com")
|
||||||
.addConverterFactory(MoshiConverterFactory.create())
|
.addConverterFactory(json.asConverterFactory(contentType))
|
||||||
.build()
|
.build()
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.henryhiles.qweather.domain.geocoding
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GeocodingData(
|
||||||
|
val location: String,
|
||||||
|
val longitude: Float,
|
||||||
|
val latitude: Float
|
||||||
|
)
|
|
@ -41,9 +41,7 @@ abstract class BasePreferenceManager(
|
||||||
getter: (key: String, defaultValue: T) -> T,
|
getter: (key: String, defaultValue: T) -> T,
|
||||||
private val setter: (key: String, newValue: T) -> Unit
|
private val setter: (key: String, newValue: T) -> Unit
|
||||||
) {
|
) {
|
||||||
@Suppress("RedundantSetter")
|
private var value by mutableStateOf(getter(key, defaultValue))
|
||||||
var value by mutableStateOf(getter(key, defaultValue))
|
|
||||||
private set
|
|
||||||
|
|
||||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
|
||||||
|
@ -102,7 +100,6 @@ abstract class BasePreferenceManager(
|
||||||
setter = ::putColor
|
setter = ::putColor
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
protected inline fun <reified E : Enum<E>> enumPreference(
|
protected inline fun <reified E : Enum<E>> enumPreference(
|
||||||
key: String,
|
key: String,
|
||||||
defaultValue: E
|
defaultValue: E
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.henryhiles.qweather.domain.mappers
|
||||||
|
|
||||||
|
import com.henryhiles.qweather.domain.remote.GeocodingDto
|
||||||
|
import com.henryhiles.qweather.domain.geocoding.GeocodingData
|
||||||
|
|
||||||
|
fun GeocodingDto.toGeocodingData(): List<GeocodingData> {
|
||||||
|
return results.map {
|
||||||
|
GeocodingData(
|
||||||
|
location = "${it.city}, ${it.admin}, ${it.country}",
|
||||||
|
longitude = it.longitude,
|
||||||
|
latitude = it.latitude,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun HourlyWeatherDataDto.toHourlyWeatherDataMap(): List<HourlyWeatherData> {
|
fun HourlyWeatherDataDto.toHourlyWeatherData(): List<HourlyWeatherData> {
|
||||||
return time.subList(0, 24).mapIndexed { index, time ->
|
return time.subList(0, 24).mapIndexed { index, time ->
|
||||||
HourlyWeatherData(
|
HourlyWeatherData(
|
||||||
time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME),
|
time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME),
|
||||||
|
@ -20,12 +20,12 @@ fun HourlyWeatherDataDto.toHourlyWeatherDataMap(): List<HourlyWeatherData> {
|
||||||
apparentTemperature = apparentTemperature[index].roundToInt(),
|
apparentTemperature = apparentTemperature[index].roundToInt(),
|
||||||
windSpeed = windSpeed[index].roundToInt(),
|
windSpeed = windSpeed[index].roundToInt(),
|
||||||
precipitationProbability = precipitationProbability.getOrNull(index),
|
precipitationProbability = precipitationProbability.getOrNull(index),
|
||||||
weatherType = WeatherType.fromWMO(weatherCode[index])
|
weatherType = WeatherType.fromWMO(weatherCode[index]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DailyWeatherDataDto.toDailyWeatherDataMap(): List<DailyWeatherData> {
|
fun DailyWeatherDataDto.toDailyWeatherData(): List<DailyWeatherData> {
|
||||||
return date.mapIndexed { index, date ->
|
return date.mapIndexed { index, date ->
|
||||||
DailyWeatherData(
|
DailyWeatherData(
|
||||||
date = LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
|
date = LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
|
||||||
|
@ -41,13 +41,16 @@ fun DailyWeatherDataDto.toDailyWeatherDataMap(): List<DailyWeatherData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun WeatherDto.toHourlyWeatherInfo(): HourlyWeatherInfo {
|
fun WeatherDto.toHourlyWeatherInfo(): HourlyWeatherInfo {
|
||||||
val weatherDataMap = hourlyWeatherData.toHourlyWeatherDataMap()
|
val weatherDataMap = hourlyWeatherData.toHourlyWeatherData()
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
val currentWeatherData = weatherDataMap.find {
|
val currentWeatherData = weatherDataMap.find {
|
||||||
it.time.hour == now.hour
|
it.time.hour == now.hour
|
||||||
}
|
}
|
||||||
return HourlyWeatherInfo(
|
return HourlyWeatherInfo(
|
||||||
weatherData = weatherDataMap,
|
weatherData = weatherDataMap,
|
||||||
currentWeatherData = currentWeatherData
|
currentWeatherData = currentWeatherData,
|
||||||
|
highTemperature = weatherDataMap.maxBy { it.temperature }.temperature,
|
||||||
|
lowTemperature = weatherDataMap.minBy { it.temperature }.temperature,
|
||||||
|
precipitationProbability = weatherDataMap.maxBy { it.precipitationProbability ?: 0}.precipitationProbability
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,24 +1,26 @@
|
||||||
package com.henryhiles.qweather.domain.remote
|
package com.henryhiles.qweather.domain.remote
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class DailyWeatherDataDto(
|
data class DailyWeatherDataDto(
|
||||||
@field:Json(name = "time")
|
@SerialName("time")
|
||||||
val date: List<String>,
|
val date: List<String>,
|
||||||
@field:Json(name = "weathercode")
|
@SerialName("weathercode")
|
||||||
val weatherCode: List<Int>,
|
val weatherCode: List<Int>,
|
||||||
@field:Json(name = "precipitation_probability_max")
|
@SerialName("precipitation_probability_max")
|
||||||
val precipitationProbabilityMax: List<Int>,
|
val precipitationProbabilityMax: List<Int?>,
|
||||||
@field:Json(name = "precipitation_sum")
|
@SerialName("precipitation_sum")
|
||||||
val precipitationSum: List<Float>,
|
val precipitationSum: List<Float>,
|
||||||
@field:Json(name = "windspeed_10m_max")
|
@SerialName("windspeed_10m_max")
|
||||||
val windSpeedMax: List<Float>,
|
val windSpeedMax: List<Float>,
|
||||||
@field:Json(name = "temperature_2m_max")
|
@SerialName("temperature_2m_max")
|
||||||
val temperatureMax: List<Float>,
|
val temperatureMax: List<Float>,
|
||||||
@field:Json(name = "temperature_2m_min")
|
@SerialName("temperature_2m_min")
|
||||||
val temperatureMin: List<Float>,
|
val temperatureMin: List<Float>,
|
||||||
@field:Json(name = "apparent_temperature_max")
|
@SerialName("apparent_temperature_max")
|
||||||
val apparentTemperatureMax: List<Float>,
|
val apparentTemperatureMax: List<Float>,
|
||||||
@field:Json(name = "apparent_temperature_min")
|
@SerialName("apparent_temperature_min")
|
||||||
val apparentTemperatureMin: List<Float>
|
val apparentTemperatureMin: List<Float>
|
||||||
)
|
)
|
|
@ -4,8 +4,9 @@ import retrofit2.http.GET
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface GeocodingApi {
|
interface GeocodingApi {
|
||||||
@GET("v1/search?count=10")
|
@GET("v1/search")
|
||||||
suspend fun getGeocodingData(
|
suspend fun getGeocodingData(
|
||||||
@Query("name") location: String,
|
@Query("name") location: String,
|
||||||
|
@Query("count") count: Int = 10
|
||||||
): GeocodingDto
|
): GeocodingDto
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package com.henryhiles.qweather.domain.remote
|
package com.henryhiles.qweather.domain.remote
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class GeocodingDto(
|
data class GeocodingDto(
|
||||||
@field:Json(name = "results")
|
val results: List<GeocodingLocationDto> = listOf()
|
||||||
val results: List<GeocodingLocationDto>
|
|
||||||
)
|
)
|
|
@ -1,16 +1,15 @@
|
||||||
package com.henryhiles.qweather.domain.remote
|
package com.henryhiles.qweather.domain.remote
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class GeocodingLocationDto(
|
data class GeocodingLocationDto(
|
||||||
@field:Json(name = "name")
|
@SerialName("name")
|
||||||
val city: String,
|
val city: String,
|
||||||
@field:Json(name = "country")
|
|
||||||
val country: String,
|
val country: String,
|
||||||
@field:Json(name = "admin1")
|
@SerialName("admin1")
|
||||||
val admin: String,
|
val admin: String,
|
||||||
@field:Json(name = "latitude")
|
|
||||||
val latitude: Float,
|
val latitude: Float,
|
||||||
@field:Json(name = "longitude")
|
|
||||||
val longitude: Float
|
val longitude: Float
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
package com.henryhiles.qweather.domain.remote
|
package com.henryhiles.qweather.domain.remote
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class HourlyWeatherDataDto(
|
data class HourlyWeatherDataDto(
|
||||||
@field:Json(name = "time")
|
|
||||||
val time: List<String>,
|
val time: List<String>,
|
||||||
@field:Json(name = "temperature_2m")
|
@SerialName("temperature_2m")
|
||||||
val temperature: List<Float>,
|
val temperature: List<Float>,
|
||||||
@field:Json(name = "apparent_temperature")
|
@SerialName("apparent_temperature")
|
||||||
val apparentTemperature: List<Float>,
|
val apparentTemperature: List<Float>,
|
||||||
@field:Json(name = "weathercode")
|
@SerialName("weathercode")
|
||||||
val weatherCode: List<Int>,
|
val weatherCode: List<Int>,
|
||||||
@field:Json(name = "precipitation_probability")
|
@SerialName("precipitation_probability")
|
||||||
val precipitationProbability: List<Int>,
|
val precipitationProbability: List<Int?>,
|
||||||
@field:Json(name = "windspeed_10m")
|
@SerialName("windspeed_10m")
|
||||||
val windSpeed: List<Float>,
|
val windSpeed: List<Float>,
|
||||||
)
|
)
|
|
@ -1,11 +1,13 @@
|
||||||
package com.henryhiles.qweather.domain.remote
|
package com.henryhiles.qweather.domain.remote
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class WeatherDto(
|
data class WeatherDto(
|
||||||
@field:Json(name = "hourly")
|
@SerialName("hourly")
|
||||||
val hourlyWeatherData: HourlyWeatherDataDto,
|
val hourlyWeatherData: HourlyWeatherDataDto,
|
||||||
|
|
||||||
@field:Json(name = "daily")
|
@SerialName("daily")
|
||||||
val dailyWeatherData: DailyWeatherDataDto
|
val dailyWeatherData: DailyWeatherDataDto
|
||||||
)
|
)
|
|
@ -1,14 +1,15 @@
|
||||||
package com.henryhiles.qweather.domain.repository
|
package com.henryhiles.qweather.domain.repository
|
||||||
|
|
||||||
|
import com.henryhiles.qweather.domain.mappers.toGeocodingData
|
||||||
import com.henryhiles.qweather.domain.remote.GeocodingApi
|
import com.henryhiles.qweather.domain.remote.GeocodingApi
|
||||||
import com.henryhiles.qweather.domain.remote.GeocodingLocationDto
|
|
||||||
import com.henryhiles.qweather.domain.util.Resource
|
import com.henryhiles.qweather.domain.util.Resource
|
||||||
|
import com.henryhiles.qweather.domain.geocoding.GeocodingData
|
||||||
|
|
||||||
class GeocodingRepository(private val api: GeocodingApi) {
|
class GeocodingRepository(private val api: GeocodingApi) {
|
||||||
suspend fun getGeocodingData(location: String): Resource<List<GeocodingLocationDto>> {
|
suspend fun getGeocodingData(location: String): Resource<List<GeocodingData>> {
|
||||||
return try {
|
return try {
|
||||||
Resource.Success(
|
Resource.Success(
|
||||||
data = api.getGeocodingData(location = location).results
|
data = api.getGeocodingData(location = location).toGeocodingData()
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.henryhiles.qweather.domain.repository
|
package com.henryhiles.qweather.domain.repository
|
||||||
|
|
||||||
import com.henryhiles.qweather.domain.mappers.toDailyWeatherDataMap
|
import com.henryhiles.qweather.domain.mappers.toDailyWeatherData
|
||||||
import com.henryhiles.qweather.domain.mappers.toHourlyWeatherInfo
|
import com.henryhiles.qweather.domain.mappers.toHourlyWeatherInfo
|
||||||
import com.henryhiles.qweather.domain.remote.WeatherApi
|
import com.henryhiles.qweather.domain.remote.WeatherApi
|
||||||
import com.henryhiles.qweather.domain.util.Resource
|
import com.henryhiles.qweather.domain.util.Resource
|
||||||
|
@ -43,7 +43,7 @@ class WeatherRepository(private val api: WeatherApi) {
|
||||||
) else api.getWeatherDataWithoutCache(
|
) else api.getWeatherDataWithoutCache(
|
||||||
lat = lat,
|
lat = lat,
|
||||||
long = long
|
long = long
|
||||||
)).dailyWeatherData.toDailyWeatherDataMap()
|
)).dailyWeatherData.toDailyWeatherData()
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
|
@ -2,5 +2,8 @@ package com.henryhiles.qweather.domain.weather
|
||||||
|
|
||||||
data class HourlyWeatherInfo(
|
data class HourlyWeatherInfo(
|
||||||
val weatherData: List<HourlyWeatherData>,
|
val weatherData: List<HourlyWeatherData>,
|
||||||
val currentWeatherData: HourlyWeatherData?
|
val currentWeatherData: HourlyWeatherData?,
|
||||||
|
val highTemperature: Int,
|
||||||
|
val lowTemperature: Int,
|
||||||
|
val precipitationProbability: Int?
|
||||||
)
|
)
|
|
@ -7,7 +7,6 @@ import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.SlideTransition
|
import cafe.adriel.voyager.transitions.SlideTransition
|
||||||
|
@ -32,11 +31,10 @@ class QWeatherActivity : ComponentActivity() {
|
||||||
Theme.LIGHT -> false
|
Theme.LIGHT -> false
|
||||||
Theme.DARK -> true
|
Theme.DARK -> true
|
||||||
}
|
}
|
||||||
val isLocationSet = location.location != ""
|
val isLocationSet = location.getLocations().isNotEmpty()
|
||||||
|
|
||||||
WeatherAppTheme(darkTheme = isDark, monet = prefs.monet) {
|
WeatherAppTheme(darkTheme = isDark, monet = prefs.monet) {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
Text(text = location.location)
|
|
||||||
Navigator(
|
Navigator(
|
||||||
screen = if (isLocationSet) MainScreen() else LocationPickerScreen(),
|
screen = if (isLocationSet) MainScreen() else LocationPickerScreen(),
|
||||||
onBackPressed = {
|
onBackPressed = {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
inline fun <reified E : Enum<E>> EnumRadioController(
|
inline fun <reified E : Enum<E>> EnumRadioController(
|
||||||
|
@ -19,7 +18,6 @@ inline fun <reified E : Enum<E>> EnumRadioController(
|
||||||
crossinline onChoiceSelected: (E) -> Unit
|
crossinline onChoiceSelected: (E) -> Unit
|
||||||
) {
|
) {
|
||||||
var choice by remember { mutableStateOf(default) }
|
var choice by remember { mutableStateOf(default) }
|
||||||
val ctx = LocalContext.current
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
enumValues<E>().forEach {
|
enumValues<E>().forEach {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.henryhiles.qweather.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalDivider(modifier: Modifier = Modifier) {
|
||||||
|
Divider(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(1.dp)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.henryhiles.qweather.presentation.components.location
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import com.henryhiles.qweather.R
|
||||||
|
import com.henryhiles.qweather.presentation.screen.LocationPickerScreen
|
||||||
|
import com.henryhiles.qweather.presentation.screenmodel.LocationPreferenceManager
|
||||||
|
import org.koin.androidx.compose.get
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LocationsDrawer(drawerState: DrawerState, children: @Composable () -> Unit) {
|
||||||
|
val location: LocationPreferenceManager = get()
|
||||||
|
val navigator = LocalNavigator.current?.parent
|
||||||
|
|
||||||
|
ModalNavigationDrawer(drawerContent = {
|
||||||
|
ModalDrawerSheet {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
val locations = location.getLocations()
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.locations),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
locations.forEachIndexed { index, data ->
|
||||||
|
NavigationDrawerItem(
|
||||||
|
label = { Text(text = data.location) },
|
||||||
|
selected = index == location.selectedLocation,
|
||||||
|
onClick = { location.selectedLocation = index },
|
||||||
|
badge = {
|
||||||
|
IconButton(onClick = { location.removeLocation(data) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
id = R.string.action_delete
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
NavigationDrawerItem(
|
||||||
|
label = { Text(text = stringResource(id = R.string.location_add)) },
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = stringResource(id = R.string.location_add)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selected = true,
|
||||||
|
onClick = { navigator?.push(LocationPickerScreen()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, drawerState = drawerState) {
|
||||||
|
children()
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,13 +7,14 @@ import androidx.compose.runtime.Composable
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
fun SmallToolbar(
|
fun SmallToolbar(
|
||||||
|
backButton: Boolean = true,
|
||||||
title: @Composable () -> Unit,
|
title: @Composable () -> Unit,
|
||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
backButton: Boolean = true
|
navigationIcon: @Composable () -> Unit = { if (backButton) BackButton() },
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = title,
|
title = title,
|
||||||
navigationIcon = { if (backButton) BackButton() },
|
navigationIcon = navigationIcon,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -6,19 +6,18 @@ import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Thermostat
|
import androidx.compose.material.icons.outlined.Thermostat
|
||||||
|
import androidx.compose.material.icons.outlined.WaterDrop
|
||||||
|
import androidx.compose.material.icons.outlined.WindPower
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.henryhiles.qweather.R
|
|
||||||
import com.henryhiles.qweather.domain.weather.HourlyWeatherData
|
import com.henryhiles.qweather.domain.weather.HourlyWeatherData
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@ -31,7 +30,7 @@ fun WeatherCard(hour: HourlyWeatherData?, location: String, modifier: Modifier =
|
||||||
}
|
}
|
||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
modifier = modifier.padding(16.dp)
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -58,7 +57,7 @@ fun WeatherCard(hour: HourlyWeatherData?, location: String, modifier: Modifier =
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = it.weatherType.iconRes),
|
painter = painterResource(id = it.weatherType.iconRes),
|
||||||
contentDescription = "Image of ${it.weatherType.weatherDesc}",
|
contentDescription = "Image of ${it.weatherType.weatherDesc}",
|
||||||
modifier = Modifier.width(200.dp)
|
modifier = Modifier.height(152.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(text = "${it.temperature}°C", fontSize = 50.sp)
|
Text(text = "${it.temperature}°C", fontSize = 50.sp)
|
||||||
|
@ -72,19 +71,19 @@ fun WeatherCard(hour: HourlyWeatherData?, location: String, modifier: Modifier =
|
||||||
WeatherDataDisplay(
|
WeatherDataDisplay(
|
||||||
value = it.apparentTemperature,
|
value = it.apparentTemperature,
|
||||||
unit = "°C",
|
unit = "°C",
|
||||||
icon = Icons.Default.Thermostat,
|
icon = Icons.Outlined.Thermostat,
|
||||||
description = "Feels like",
|
description = "Feels like",
|
||||||
)
|
)
|
||||||
WeatherDataDisplay(
|
WeatherDataDisplay(
|
||||||
value = it.precipitationProbability,
|
value = it.precipitationProbability,
|
||||||
unit = "%",
|
unit = "%",
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.ic_drop),
|
icon = Icons.Outlined.WaterDrop,
|
||||||
description = "Chance of precipitation"
|
description = "Chance of precipitation"
|
||||||
)
|
)
|
||||||
WeatherDataDisplay(
|
WeatherDataDisplay(
|
||||||
value = it.windSpeed,
|
value = it.windSpeed,
|
||||||
unit = "km/h",
|
unit = "km/h",
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.ic_wind),
|
icon = Icons.Outlined.WindPower,
|
||||||
description = "Wind Speed",
|
description = "Wind Speed",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Water
|
import androidx.compose.material.icons.outlined.Water
|
||||||
import androidx.compose.material.icons.outlined.WaterDrop
|
import androidx.compose.material.icons.outlined.WaterDrop
|
||||||
|
import androidx.compose.material.icons.outlined.WindPower
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
@ -15,11 +16,8 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.henryhiles.qweather.R
|
|
||||||
import com.henryhiles.qweather.domain.weather.DailyWeatherData
|
import com.henryhiles.qweather.domain.weather.DailyWeatherData
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@ -71,7 +69,7 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData, expanded: Boolean, onExpand:
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp, 0.dp, 16.dp, 16.dp),
|
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
WeatherDataDisplay(
|
WeatherDataDisplay(
|
||||||
|
@ -80,7 +78,6 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData, expanded: Boolean, onExpand:
|
||||||
icon = Icons.Outlined.WaterDrop,
|
icon = Icons.Outlined.WaterDrop,
|
||||||
description = "Chance of rain"
|
description = "Chance of rain"
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
WeatherDataDisplay(
|
WeatherDataDisplay(
|
||||||
value = dailyWeatherData.windSpeedMax,
|
value = dailyWeatherData.windSpeedMax,
|
||||||
|
@ -88,12 +85,11 @@ fun WeatherDay(dailyWeatherData: DailyWeatherData, expanded: Boolean, onExpand:
|
||||||
icon = Icons.Outlined.Water,
|
icon = Icons.Outlined.Water,
|
||||||
description = "Precipitation Amount"
|
description = "Precipitation Amount"
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
WeatherDataDisplay(
|
WeatherDataDisplay(
|
||||||
value = dailyWeatherData.windSpeedMax,
|
value = dailyWeatherData.windSpeedMax,
|
||||||
unit = "km/h",
|
unit = "km/h",
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.ic_wind),
|
icon = Icons.Outlined.WindPower,
|
||||||
description = "Wind Speed"
|
description = "Wind Speed"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
package com.henryhiles.qweather.presentation.components.weather
|
package com.henryhiles.qweather.presentation.components.weather
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherState
|
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherState
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@ -19,24 +17,15 @@ fun WeatherForecast(
|
||||||
onChangeSelected: (Int) -> Unit
|
onChangeSelected: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
state.hourlyWeatherInfo?.weatherData?.let {
|
state.hourlyWeatherInfo?.weatherData?.let {
|
||||||
Column(
|
val rowState = rememberLazyListState(LocalDateTime.now().hour)
|
||||||
modifier = modifier
|
LazyRow(state = rowState, modifier = modifier) {
|
||||||
.fillMaxWidth()
|
itemsIndexed(it) { index, data ->
|
||||||
.padding(horizontal = 16.dp)
|
WeatherHour(
|
||||||
) {
|
data = data,
|
||||||
Text(text = "Today", fontSize = 20.sp)
|
modifier = Modifier
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
.padding(horizontal = 8.dp),
|
||||||
val rowState = rememberLazyListState(LocalDateTime.now().hour)
|
onChangeSelected = { onChangeSelected(index) }
|
||||||
|
)
|
||||||
LazyRow(state = rowState) {
|
|
||||||
itemsIndexed(it) { index, data ->
|
|
||||||
WeatherHour(
|
|
||||||
data = data,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
onChangeSelected = { onChangeSelected(index) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,13 +37,11 @@ fun WeatherHour(
|
||||||
horizontalAlignment = CenterHorizontally
|
horizontalAlignment = CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(text = formattedTime)
|
Text(text = formattedTime)
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = it.weatherType.iconRes),
|
painter = painterResource(id = it.weatherType.iconRes),
|
||||||
contentDescription = "Image of ${it.weatherType.weatherDesc}",
|
contentDescription = "Image of ${it.weatherType.weatherDesc}",
|
||||||
modifier = Modifier.width(40.dp)
|
modifier = Modifier.width(40.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(text = "${it.temperature}°C")
|
Text(text = "${it.temperature}°C")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.henryhiles.qweather.presentation.components.weather
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.henryhiles.qweather.R
|
||||||
|
import com.henryhiles.qweather.presentation.components.VerticalDivider
|
||||||
|
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WeatherToday(state: HourlyWeatherState) {
|
||||||
|
state.hourlyWeatherInfo?.let {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(24.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.weather_high, it.highTemperature),
|
||||||
|
)
|
||||||
|
VerticalDivider(modifier = Modifier.padding(horizontal = 8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.weather_low, it.lowTemperature)
|
||||||
|
)
|
||||||
|
VerticalDivider(modifier = Modifier.padding(horizontal = 8.dp))
|
||||||
|
Text(
|
||||||
|
text = it.precipitationProbability?.let {
|
||||||
|
stringResource(
|
||||||
|
id = R.string.weather_precipitation,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
} ?: stringResource(
|
||||||
|
id = R.string.unknown
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,6 @@ import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
@ -24,6 +23,7 @@ import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.koin.getScreenModel
|
import cafe.adriel.voyager.koin.getScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import com.henryhiles.qweather.R
|
import com.henryhiles.qweather.R
|
||||||
|
import com.henryhiles.qweather.domain.geocoding.GeocodingData
|
||||||
import com.henryhiles.qweather.presentation.components.navigation.SmallToolbar
|
import com.henryhiles.qweather.presentation.components.navigation.SmallToolbar
|
||||||
import com.henryhiles.qweather.presentation.screenmodel.LocationPickerScreenModel
|
import com.henryhiles.qweather.presentation.screenmodel.LocationPickerScreenModel
|
||||||
|
|
||||||
|
@ -32,24 +32,20 @@ class LocationPickerScreen : Screen {
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val screenModel: LocationPickerScreenModel = getScreenModel()
|
val screenModel: LocationPickerScreenModel = getScreenModel()
|
||||||
var latitude by remember { mutableStateOf(screenModel.prefs.latitude) }
|
var location by remember {
|
||||||
var longitude by remember { mutableStateOf(screenModel.prefs.longitude) }
|
mutableStateOf<GeocodingData?>(null)
|
||||||
var location by remember { mutableStateOf(screenModel.prefs.location) }
|
}
|
||||||
var locationSearch by remember { mutableStateOf("") }
|
var locationSearch by remember { mutableStateOf("") }
|
||||||
var isAboutOpen by remember { mutableStateOf(false) }
|
var isAboutOpen by remember { mutableStateOf(false) }
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Scaffold(modifier = Modifier.imePadding(),
|
Scaffold(modifier = Modifier.imePadding(),
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(onClick = {
|
FloatingActionButton(onClick = {
|
||||||
if (location == "") isAboutOpen = true
|
location?.let {
|
||||||
else {
|
screenModel.prefs.addLocation(it)
|
||||||
screenModel.prefs.location = location
|
|
||||||
screenModel.prefs.latitude = latitude
|
|
||||||
screenModel.prefs.longitude = longitude
|
|
||||||
navigator?.push(MainScreen())
|
navigator?.push(MainScreen())
|
||||||
}
|
} ?: kotlin.run { isAboutOpen = true }
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
|
@ -57,32 +53,32 @@ class LocationPickerScreen : Screen {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
screenModel.state.error?.let {
|
Column {
|
||||||
AlertDialog(
|
SmallToolbar(
|
||||||
onDismissRequest = {},
|
title = { Text(text = stringResource(id = R.string.location_choose)) },
|
||||||
confirmButton = {},
|
actions = {
|
||||||
title = { Text(text = stringResource(id = R.string.error)) },
|
IconButton(
|
||||||
text = {
|
onClick = { isAboutOpen = true }) {
|
||||||
SelectionContainer {
|
Icon(
|
||||||
Text(
|
imageVector = Icons.Outlined.Info,
|
||||||
text = it,
|
contentDescription = stringResource(id = R.string.help_screen)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
)
|
screenModel.state.error?.let {
|
||||||
} ?: kotlin.run {
|
AlertDialog(
|
||||||
Column {
|
onDismissRequest = {},
|
||||||
SmallToolbar(
|
confirmButton = {},
|
||||||
title = { Text(text = stringResource(id = R.string.location_choose)) },
|
title = { Text(text = stringResource(id = R.string.error)) },
|
||||||
actions = {
|
text = {
|
||||||
IconButton(
|
SelectionContainer {
|
||||||
onClick = { isAboutOpen = true }) {
|
Text(
|
||||||
Icon(
|
text = it,
|
||||||
imageVector = Icons.Outlined.Info,
|
|
||||||
contentDescription = stringResource(id = R.string.help_screen)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
} ?: kotlin.run {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
if (isAboutOpen) AlertDialog(
|
if (isAboutOpen) AlertDialog(
|
||||||
title = { Text(text = stringResource(id = R.string.location_choose)) },
|
title = { Text(text = stringResource(id = R.string.location_choose)) },
|
||||||
|
@ -134,27 +130,16 @@ class LocationPickerScreen : Screen {
|
||||||
) else screenModel.state.locations?.let {
|
) else screenModel.state.locations?.let {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(it) {
|
items(it) {
|
||||||
val locationText by remember {
|
val selected = it == location
|
||||||
mutableStateOf(
|
|
||||||
context.getString(
|
|
||||||
R.string.location_string,
|
|
||||||
it.city, it.admin, it.country
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Card(modifier = Modifier.clickable {
|
Card(modifier = Modifier.clickable { location = it }) {
|
||||||
location = locationText
|
|
||||||
longitude = it.longitude
|
|
||||||
latitude = it.latitude
|
|
||||||
}) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (location == locationText) {
|
if (selected) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
contentDescription = stringResource(
|
contentDescription = stringResource(
|
||||||
|
@ -164,7 +149,7 @@ class LocationPickerScreen : Screen {
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text(text = locationText)
|
Text(text = it.location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,67 @@
|
||||||
package com.henryhiles.qweather.presentation.screen
|
package com.henryhiles.qweather.presentation.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.CurrentScreen
|
import cafe.adriel.voyager.navigator.CurrentScreen
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
||||||
|
import com.henryhiles.qweather.R
|
||||||
import com.henryhiles.qweather.domain.util.NavigationTab
|
import com.henryhiles.qweather.domain.util.NavigationTab
|
||||||
|
import com.henryhiles.qweather.presentation.components.location.LocationsDrawer
|
||||||
import com.henryhiles.qweather.presentation.components.navigation.BottomBar
|
import com.henryhiles.qweather.presentation.components.navigation.BottomBar
|
||||||
import com.henryhiles.qweather.presentation.components.navigation.SmallToolbar
|
import com.henryhiles.qweather.presentation.components.navigation.SmallToolbar
|
||||||
|
import com.henryhiles.qweather.presentation.screenmodel.LocationPreferenceManager
|
||||||
import com.henryhiles.qweather.presentation.tabs.TodayTab
|
import com.henryhiles.qweather.presentation.tabs.TodayTab
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.get
|
||||||
|
|
||||||
class MainScreen : Screen {
|
class MainScreen : Screen {
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
|
val drawerState =
|
||||||
|
rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
TabNavigator(tab = TodayTab) {
|
TabNavigator(tab = TodayTab) {
|
||||||
Scaffold(
|
LocationsDrawer(drawerState = drawerState) {
|
||||||
topBar = {
|
Scaffold(
|
||||||
SmallToolbar(
|
topBar = {
|
||||||
title = { Text(text = "QWeather") },
|
SmallToolbar(
|
||||||
actions = {
|
title = { Text(text = stringResource(R.string.app_name)) },
|
||||||
(it.current as? NavigationTab)?.Actions()
|
actions = {
|
||||||
|
(it.current as? NavigationTab)?.Actions()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
with(drawerState) { if (isOpen) close() else open() }
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Menu,
|
||||||
|
contentDescription = stringResource(id = R.string.location_picker_open)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
},
|
bottomBar = {
|
||||||
bottomBar = {
|
BottomBar(navigator = it)
|
||||||
BottomBar(navigator = it)
|
}
|
||||||
}
|
) { padding ->
|
||||||
) { padding ->
|
Box(modifier = Modifier.padding(padding)) {
|
||||||
Box(modifier = Modifier.padding(padding)) {
|
CurrentScreen()
|
||||||
CurrentScreen()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,20 +19,20 @@ data class DailyWeatherState(
|
||||||
|
|
||||||
class DailyWeatherScreenModel(
|
class DailyWeatherScreenModel(
|
||||||
private val repository: WeatherRepository,
|
private val repository: WeatherRepository,
|
||||||
private val location: LocationPreferenceManager
|
locationPreferenceManager: LocationPreferenceManager
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
var state by mutableStateOf(DailyWeatherState())
|
var state by mutableStateOf(DailyWeatherState())
|
||||||
private set
|
private set
|
||||||
|
val location = locationPreferenceManager.getSelectedLocation()
|
||||||
|
|
||||||
fun loadWeatherInfo(cache: Boolean = true) {
|
fun loadWeatherInfo(cache: Boolean = true) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
state = state.copy(isLoading = true, error = null)
|
state = state.copy(isLoading = true, error = null)
|
||||||
state = when (val result =
|
state = when (val result = repository.getDailyWeatherData(
|
||||||
repository.getDailyWeatherData(
|
lat = location.latitude,
|
||||||
lat = location.latitude,
|
long = location.longitude,
|
||||||
long = location.longitude,
|
cache = cache
|
||||||
cache = cache
|
)) {
|
||||||
)) {
|
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
state.copy(
|
state.copy(
|
||||||
dailyWeatherData = result.data,
|
dailyWeatherData = result.data,
|
||||||
|
|
|
@ -19,11 +19,13 @@ data class HourlyWeatherState(
|
||||||
|
|
||||||
class HourlyWeatherScreenModel(
|
class HourlyWeatherScreenModel(
|
||||||
private val repository: WeatherRepository,
|
private val repository: WeatherRepository,
|
||||||
val location: LocationPreferenceManager,
|
locationPreferenceManager: LocationPreferenceManager,
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
var state by mutableStateOf(HourlyWeatherState())
|
var state by mutableStateOf(HourlyWeatherState())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
val location = locationPreferenceManager.getSelectedLocation()
|
||||||
|
|
||||||
fun loadWeatherInfo(cache: Boolean = true) {
|
fun loadWeatherInfo(cache: Boolean = true) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
state = state.copy(isLoading = true, error = null, selected = null)
|
state = state.copy(isLoading = true, error = null, selected = null)
|
||||||
|
|
|
@ -6,23 +6,46 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.coroutineScope
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import com.henryhiles.qweather.domain.geocoding.GeocodingData
|
||||||
import com.henryhiles.qweather.domain.manager.BasePreferenceManager
|
import com.henryhiles.qweather.domain.manager.BasePreferenceManager
|
||||||
import com.henryhiles.qweather.domain.remote.GeocodingLocationDto
|
|
||||||
import com.henryhiles.qweather.domain.repository.GeocodingRepository
|
import com.henryhiles.qweather.domain.repository.GeocodingRepository
|
||||||
import com.henryhiles.qweather.domain.util.Resource
|
import com.henryhiles.qweather.domain.util.Resource
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
data class LocationPickerState(
|
data class LocationPickerState(
|
||||||
val locations: List<GeocodingLocationDto>? = null,
|
val locations: List<GeocodingData>? = null,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class LocationPreferenceManager(context: Context) :
|
class LocationPreferenceManager(context: Context) :
|
||||||
BasePreferenceManager(context.getSharedPreferences("location", Context.MODE_PRIVATE)) {
|
BasePreferenceManager(context.getSharedPreferences("location", Context.MODE_PRIVATE)) {
|
||||||
var latitude by floatPreference("lat", 0f)
|
private var locations by stringPreference(
|
||||||
var longitude by floatPreference("long", 0f)
|
"locations",
|
||||||
var location by stringPreference("string")
|
Json.encodeToString(value = listOf<GeocodingData>())
|
||||||
|
)
|
||||||
|
var selectedLocation by intPreference("selected_location", 0)
|
||||||
|
|
||||||
|
fun getSelectedLocation(): GeocodingData {
|
||||||
|
return getLocations()[selectedLocation]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocations(): List<GeocodingData> {
|
||||||
|
return Json.decodeFromString(string = locations)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addLocation(location: GeocodingData) {
|
||||||
|
val currentLocations = getLocations()
|
||||||
|
locations = Json.encodeToString(value = currentLocations + location)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeLocation(location: GeocodingData) {
|
||||||
|
val currentLocations = getLocations()
|
||||||
|
locations = Json.encodeToString(value = currentLocations - location)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocationPickerScreenModel(
|
class LocationPickerScreenModel(
|
||||||
|
|
|
@ -20,6 +20,7 @@ import com.henryhiles.qweather.R
|
||||||
import com.henryhiles.qweather.domain.util.NavigationTab
|
import com.henryhiles.qweather.domain.util.NavigationTab
|
||||||
import com.henryhiles.qweather.presentation.components.weather.WeatherCard
|
import com.henryhiles.qweather.presentation.components.weather.WeatherCard
|
||||||
import com.henryhiles.qweather.presentation.components.weather.WeatherForecast
|
import com.henryhiles.qweather.presentation.components.weather.WeatherForecast
|
||||||
|
import com.henryhiles.qweather.presentation.components.weather.WeatherToday
|
||||||
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
|
import com.henryhiles.qweather.presentation.screenmodel.HourlyWeatherScreenModel
|
||||||
|
|
||||||
object TodayTab : NavigationTab {
|
object TodayTab : NavigationTab {
|
||||||
|
@ -77,6 +78,7 @@ object TodayTab : NavigationTab {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
WeatherCard(
|
WeatherCard(
|
||||||
hour = weatherViewModel.state.selected?.let {
|
hour = weatherViewModel.state.selected?.let {
|
||||||
|
@ -84,7 +86,7 @@ object TodayTab : NavigationTab {
|
||||||
} ?: weatherViewModel.state.hourlyWeatherInfo?.currentWeatherData,
|
} ?: weatherViewModel.state.hourlyWeatherInfo?.currentWeatherData,
|
||||||
location = weatherViewModel.location.location
|
location = weatherViewModel.location.location
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
WeatherToday(state = weatherViewModel.state)
|
||||||
WeatherForecast(
|
WeatherForecast(
|
||||||
state = weatherViewModel.state
|
state = weatherViewModel.state
|
||||||
) { weatherViewModel.setSelected(it) }
|
) { weatherViewModel.setSelected(it) }
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="149.86dp"
|
|
||||||
android:height="249.77dp"
|
|
||||||
android:viewportWidth="149.86"
|
|
||||||
android:viewportHeight="249.77">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M74.93,249.77c41.32,0 74.93,-28.71 74.93,-64 0,-34 -75.48,-178 -78.7,-184.1a3.12,3.12 0,0 0,-5.62 0.2C62.87,8 0,151.88 0,185.77 0,221.06 33.61,249.77 74.93,249.77ZM68.66,10.38c14.36,27.76 75,146.61 75,175.39 0,31.84 -30.82,57.75 -68.69,57.75S6.24,217.61 6.24,185.77C6.24,157 56.53,38.52 68.66,10.38ZM13.11,190.91a3.12,3.12 0,0 1,2.62 -3.55A3.15,3.15 0,0 1,19.28 190c2.64,17.47 15.64,31.71 34.78,38.09a3.12,3.12 0,1 1,-2 5.93C31,227 16.06,210.46 13.11,190.91Z"/>
|
|
||||||
</vector>
|
|
|
@ -1,33 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="212.3dp"
|
|
||||||
android:height="62.44dp"
|
|
||||||
android:viewportWidth="212.3"
|
|
||||||
android:viewportHeight="62.44">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M209.18,6.24H193.57a3.12,3.12 0,0 1,0 -6.24h15.61a3.12,3.12 0,1 1,0 6.24Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M181.08,6.24H178A3.12,3.12 0,0 1,178 0h3.12a3.12,3.12 0,0 1,0 6.24Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M165.47,6.24H3.12A3.12,3.12 0,0 1,3.12 0H165.47a3.12,3.12 0,1 1,0 6.24Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M181.08,43.71H31.22a3.13,3.13 0,0 1,0 -6.25H181.08a3.13,3.13 0,0 1,0 6.25Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M165.47,62.44H106.15a3.12,3.12 0,0 1,0 -6.24h59.32a3.12,3.12 0,1 1,0 6.24Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M93.66,62.44H90.54a3.12,3.12 0,0 1,0 -6.24h3.12a3.12,3.12 0,1 1,0 6.24Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M78.05,62.44H46.83a3.12,3.12 0,1 1,0 -6.24H78.05a3.12,3.12 0,1 1,0 6.24Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M196.69,25h-153a3.13,3.13 0,0 1,0 -6.25h153a3.13,3.13 0,0 1,0 6.25Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M31.22,25H15.61a3.13,3.13 0,0 1,0 -6.25H31.22a3.13,3.13 0,0 1,0 6.25Z"/>
|
|
||||||
</vector>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="277.86dp"
|
|
||||||
android:height="199.81dp"
|
|
||||||
android:viewportWidth="277.86"
|
|
||||||
android:viewportHeight="199.81">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M277.86,57.76c0,27.69 -26.67,48.39 -50.53,48.39L50,106.15a3.12,3.12 0,0 1,0 -6.24L227.33,99.91c20.48,0 44.29,-18.42 44.29,-42.15 0,-23.24 -17.06,-42.15 -38,-42.15 -13.64,0 -25.89,10.36 -32.79,27.72A3.12,3.12 0,1 1,195 41c7.87,-19.82 22.3,-31.65 38.59,-31.65C258,9.37 277.86,31.07 277.86,57.76ZM237.28,93.66a3,3 0,0 0,1.14 -0.22C252,88.06 260,80.06 265.17,66.68a3.12,3.12 0,0 0,-5.83 -2.24c-4.51,11.74 -11.23,18.46 -23.21,23.2a3.12,3.12 0,0 0,1.15 6ZM122.8,125.06a3.13,3.13 0,0 0,-0.87 -0.18L34.34,124.88a3.13,3.13 0,0 0,0 6.25h84.59c18.77,0 34,13.3 34,29.66 0,15.47 -14.56,32.78 -34,32.78 -13.79,0 -25.44,-10.15 -30.85,-20.22a3.13,3.13 0,0 0,-5.5 3c6.3,11.69 20,23.49 36.35,23.49 21.46,0 40.3,-18.23 40.3,-39C159.23,142.15 143.21,126.8 122.8,125.06ZM12.8,78.06a3.12,3.12 0,0 0,3.13 3.12h76.8a3,3 0,0 0,0.61 -0.12c18,-1.24 37.8,-16 37.8,-34.8s-18,-36.88 -37,-36.88c-12.83,0 -27.36,9.71 -33.08,22.1a3.12,3.12 0,0 0,5.67 2.62c4.7,-10.19 17,-18.48 27.41,-18.48 15.52,0 30.75,15.18 30.75,30.64 0,15.89 -18.6,28.68 -34,28.68h-75A3.13,3.13 0,0 0,12.79 78.05ZM108.64,6.18C120.73,8.69 135,23 137.43,35a3.13,3.13 0,0 0,3.06 2.5,3.52 3.52,0 0,0 0.63,-0.06 3.12,3.12 0,0 0,2.43 -3.68C140.65,19.42 124.3,3.06 109.91,0.06a3.13,3.13 0,1 0,-1.27 6.12ZM215.52,168.59c-7.34,0 -16,-6.1 -19.36,-13.6a3.12,3.12 0,1 0,-5.71 2.53c4.39,9.87 15.17,17.32 25.07,17.32 14.39,0 28,-14 28,-28.85s-15,-26.29 -28.57,-27.26a3.55,3.55 0,0 0,-0.47 -0.09L196.69,118.64a3.12,3.12 0,0 0,0 6.24h16.45c10.86,0 24.14,9.74 24.14,21.11S226.5,168.59 215.52,168.59ZM184.2,118.64h-3.12a3.12,3.12 0,0 0,0 6.24h3.12a3.12,3.12 0,0 0,0 -6.24ZM168.59,118.64h-15.3a3.12,3.12 0,1 0,0 6.24h15.3a3.12,3.12 0,0 0,0 -6.24ZM37.46,99.91L34.34,99.91a3.12,3.12 0,0 0,0 6.24h3.12a3.12,3.12 0,1 0,0 -6.24ZM25,103a3.12,3.12 0,0 0,-3.13 -3.12L3.12,99.88a3.12,3.12 0,0 0,0 6.24L21.85,106.12A3.13,3.13 0,0 0,25 103ZM43.73,137.34a3.13,3.13 0,0 0,0 6.25L78.05,143.59a3.13,3.13 0,0 0,0 -6.25Z"/>
|
|
||||||
</vector>
|
|
|
@ -8,11 +8,12 @@
|
||||||
<string name="action_apply">Apply</string>
|
<string name="action_apply">Apply</string>
|
||||||
<string name="action_confirm">Confirm</string>
|
<string name="action_confirm">Confirm</string>
|
||||||
<string name="action_open_about">About</string>
|
<string name="action_open_about">About</string>
|
||||||
|
<string name="action_delete">Delete</string>
|
||||||
<string name="action_search">Search</string>
|
<string name="action_search">Search</string>
|
||||||
<string name="action_reload">Reload</string>
|
<string name="action_reload">Reload</string>
|
||||||
<string name="action_try_again">Try Again</string>
|
<string name="action_try_again">Try Again</string>
|
||||||
|
|
||||||
<string name="selected">Selected</string>x
|
<string name="selected">Selected</string>
|
||||||
|
|
||||||
<string name="help_screen">How do I use this screen?</string>
|
<string name="help_screen">How do I use this screen?</string>
|
||||||
<string name="help_location_picker">Please search a location, then tap a result. Then tap the apply button in the bottom left corner.</string>
|
<string name="help_location_picker">Please search a location, then tap a result. Then tap the apply button in the bottom left corner.</string>
|
||||||
|
@ -27,16 +28,21 @@
|
||||||
<string name="settings_location_description">Location to fetch data from</string>
|
<string name="settings_location_description">Location to fetch data from</string>
|
||||||
|
|
||||||
<string name="location">Location</string>
|
<string name="location">Location</string>
|
||||||
<string name="location_string">%1$s, %2$s, %3$s</string>
|
<string name="locations">Locations</string>
|
||||||
<string name="location_auto_pick">Auto-pick location</string>
|
<string name="location_add">Add Location</string>
|
||||||
|
<string name="location_picker_open">Open location picker</string>
|
||||||
<string name="location_choose">Choose a Location</string>
|
<string name="location_choose">Choose a Location</string>
|
||||||
|
|
||||||
<string name="theme_system">System</string>
|
<string name="theme_system">System</string>
|
||||||
<string name="theme_light">Light</string>
|
<string name="theme_light">Light</string>
|
||||||
<string name="theme_dark">Dark</string>
|
<string name="theme_dark">Dark</string>
|
||||||
|
|
||||||
|
<string name="weather_high">High: %1$d°C</string>
|
||||||
|
<string name="weather_low">Low: %1$d°C</string>
|
||||||
|
<string name="weather_precipitation">Precipitation: %1$d﹪</string>
|
||||||
|
|
||||||
<string name="unknown">Unknown</string>
|
<string name="unknown">Unknown</string>
|
||||||
|
|
||||||
<string name="error">An error occurred</string>
|
<string name="error">An error occurred</string>
|
||||||
<string name="error_location">Couldn\'t retrieve location. Make sure to grant permission and enable GPS.</string>
|
<string name="error_location">"Couldn't retrieve location. Make sure to grant permission and enable GPS."</string>
|
||||||
</resources>
|
</resources>
|
Reference in a new issue