Start adding support for multiple locations

This commit is contained in:
Henry Hiles 2023-05-02 11:50:35 -04:00
parent b8ab0605e8
commit b32701b138
35 changed files with 377 additions and 231 deletions

View file

@ -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"

View file

@ -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()
} }

View file

@ -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
)

View file

@ -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

View file

@ -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,
)
}
}

View file

@ -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
) )
} }

View file

@ -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>
) )

View file

@ -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
} }

View file

@ -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>
) )

View file

@ -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
) )

View file

@ -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>,
) )

View file

@ -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
) )

View file

@ -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()

View file

@ -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()

View file

@ -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?
) )

View file

@ -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 = {

View file

@ -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 {

View file

@ -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)
)
}

View file

@ -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()
}
}

View file

@ -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,
) )
} }

View file

@ -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",
) )
} }

View file

@ -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"
) )
} }

View file

@ -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) }
)
}
} }
} }
} }

View file

@ -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")
} }
} }

View file

@ -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
)
)
}
}
}

View file

@ -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)
} }
} }
} }

View file

@ -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() }
} }
} }
} }

View file

@ -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,

View file

@ -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)

View file

@ -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(

View file

@ -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) }

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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&#65130;</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>