Weather widget: add button to manually set location if permission is missing

This commit is contained in:
MM20 2022-01-03 23:45:33 +01:00
parent d5cedb446e
commit 61c77a137d
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 181 additions and 139 deletions

View File

@ -423,4 +423,5 @@
<string name="grant_permission">Gewähren</string>
<string name="missing_permission_auto_location">Standortzugriff wird benötigt, um den Standort automatisch zu ermitteln</string>
<string name="weather_widget_set_location">Standort festlegen</string>
</resources>

View File

@ -461,4 +461,5 @@
<string name="grant_permission">Grant</string>
<string name="missing_permission_auto_location">Location access is required to determine the location automatically</string>
<string name="weather_widget_set_location">Set location</string>
</resources>

View File

@ -0,0 +1,116 @@
package de.mm20.launcher2.ui.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.OutlinedTextField
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.launch
@Composable
fun WeatherLocationSearchDialog(
onDismissRequest: () -> Unit
) {
val scope = rememberCoroutineScope()
val viewModel : WeatherLocationSearchDialogVM = viewModel()
val isSearching by viewModel.isSearchingLocation.observeAsState(initial = false)
val locations by viewModel.locationResults.observeAsState(emptyList())
Dialog(onDismissRequest = onDismissRequest) {
Surface(
tonalElevation = 16.dp,
shadowElevation = 16.dp,
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.fillMaxSize()
.padding(vertical = 16.dp),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.preference_location),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.fillMaxWidth()
.padding(
start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp
)
)
var query by remember { mutableStateOf("") }
OutlinedTextField(
singleLine = true,
value = query,
onValueChange = {
query = it
scope.launch {
viewModel.searchLocation(it)
}
}, modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
)
if (isSearching) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.weight(1f)
.padding(vertical = 16.dp)
)
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(bottom = 16.dp)
) {
items(locations) {
Text(
text = it.name,
modifier = Modifier
.clickable {
viewModel.setLocation(it)
onDismissRequest()
}
.padding(
horizontal = 24.dp,
vertical = 16.dp
)
)
}
}
}
TextButton(
onClick = onDismissRequest,
modifier = Modifier
.align(Alignment.End)
.padding(24.dp)
) {
Text(
text = stringResource(R.string.close),
style = MaterialTheme.typography.labelLarge
)
}
}
}
}
}

View File

@ -0,0 +1,41 @@
package de.mm20.launcher2.ui.common
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.weather.WeatherLocation
import de.mm20.launcher2.weather.WeatherRepository
import kotlinx.coroutines.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.coroutineContext
class WeatherLocationSearchDialogVM: ViewModel(), KoinComponent {
private val repository: WeatherRepository by inject()
val isSearchingLocation = MutableLiveData(false)
val locationResults = MutableLiveData<List<WeatherLocation>>(emptyList())
private var debounceSearchJob: Job? = null
suspend fun searchLocation(query: String) {
debounceSearchJob?.cancelAndJoin()
if (query.isBlank()) {
locationResults.value = emptyList()
isSearchingLocation.value = false
return
}
withContext(coroutineContext) {
debounceSearchJob = launch {
delay(1000)
isSearchingLocation.value = true
locationResults.value = repository.lookupLocation(query)
isSearchingLocation.value = false
}
}
}
fun setLocation(location: WeatherLocation) {
locationResults.postValue(emptyList())
repository.setAutoLocation(false)
repository.setLocation(location)
}
}

View File

@ -29,6 +29,7 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.WeatherLocationSearchDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.weather.AnimatedWeatherIcon
import de.mm20.launcher2.ui.weather.WeatherIcon
@ -50,16 +51,32 @@ fun WeatherWidget() {
val imperialUnits by viewModel.imperialUnits.observeAsState(false)
var showLocationDialog by remember { mutableStateOf(false) }
if (showLocationDialog) {
WeatherLocationSearchDialog(onDismissRequest = { showLocationDialog = false })
}
val forecast = selectedForecast ?: run {
val hasPermission by viewModel.hasLocationPermission.observeAsState()
val autoLocation by viewModel.autoLocation.observeAsState()
AnimatedVisibility(hasPermission == false && autoLocation == true) {
MissingPermissionBanner(
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp),
text = stringResource(id = R.string.missing_permission_auto_location),
onClick = {
viewModel.requestLocationPermission(context as AppCompatActivity)
})
},
secondaryAction = {
TextButton(onClick = {
showLocationDialog = true
}) {
Text(stringResource(R.string.weather_widget_set_location), style = MaterialTheme.typography.labelLarge)
}
}
)
}
NoData()
return

View File

@ -2,36 +2,21 @@ package de.mm20.launcher2.ui.settings.weather
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.OutlinedTextField
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.Settings.WeatherSettings.WeatherProvider
import de.mm20.launcher2.ui.BuildConfig
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.WeatherLocationSearchDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.*
import de.mm20.launcher2.weather.WeatherLocation
import kotlinx.coroutines.launch
@Composable
fun WeatherScreen() {
@ -87,25 +72,11 @@ fun WeatherScreen() {
viewModel.setAutoLocation(it)
}
)
val scope = rememberCoroutineScope()
val location by viewModel.location.observeAsState()
val isSearching by viewModel.isSearchingLocation.observeAsState(initial = false)
val locations by viewModel.locationResults.observeAsState(emptyList())
LocationPreference(
title = stringResource(R.string.preference_location),
value = location,
locations = locations,
onValueChanged = {
viewModel.setLocation(it)
},
onLocationSearch = {
scope.launch {
viewModel.searchLocation(it)
}
},
enabled = !autoLocation,
isSearching = isSearching,
)
}
}
@ -117,7 +88,7 @@ fun WeatherScreen() {
summary = "Remove weather data from database",
onClick = {
viewModel.clearWeatherData()
})
})
}
}
}
@ -128,10 +99,6 @@ fun WeatherScreen() {
fun LocationPreference(
title: String,
value: WeatherLocation?,
onValueChanged: (WeatherLocation) -> Unit,
onLocationSearch: (String) -> Unit,
isSearching: Boolean,
locations: List<WeatherLocation>,
enabled: Boolean = true
) {
var showDialog by remember { mutableStateOf(false) }
@ -144,83 +111,6 @@ fun LocationPreference(
}
)
if (showDialog) {
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
tonalElevation = 16.dp,
shadowElevation = 16.dp,
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.fillMaxSize()
.padding(vertical = 16.dp),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.fillMaxWidth()
.padding(
start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp
)
)
var query by remember { mutableStateOf("") }
OutlinedTextField(
singleLine = true,
value = query,
onValueChange = {
query = it
onLocationSearch(it)
}, modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
)
if (isSearching) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.weight(1f)
.padding(vertical = 16.dp)
)
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(bottom = 16.dp)
) {
items(locations) {
Text(
text = it.name,
modifier = Modifier
.clickable {
onValueChanged(it)
showDialog = false
}
.padding(
horizontal = 24.dp,
vertical = 16.dp
)
)
}
}
}
TextButton(
onClick = { showDialog = false },
modifier = Modifier
.align(Alignment.End)
.padding(24.dp)
) {
Text(
text = stringResource(R.string.close),
style = MaterialTheme.typography.labelLarge
)
}
}
}
}
WeatherLocationSearchDialog(onDismissRequest = { showDialog = false })
}
}

View File

@ -46,28 +46,6 @@ class WeatherScreenVM : ViewModel(), KoinComponent {
}
val location = MutableLiveData<WeatherLocation?>(null)
fun setLocation(location: WeatherLocation) {
locationResults.postValue(emptyList())
repository.setLocation(location)
}
private var debounceSearchJob: Job? = null
suspend fun searchLocation(query: String) {
debounceSearchJob?.cancelAndJoin()
if (query.isBlank()) {
locationResults.value = emptyList()
isSearchingLocation.value = false
return
}
withContext(coroutineContext) {
debounceSearchJob = launch {
delay(1000)
isSearchingLocation.value = true
locationResults.value = repository.lookupLocation(query)
isSearchingLocation.value = false
}
}
}
val hasLocationPermission = permissionsManager.hasPermission(PermissionGroup.Location).asLiveData()
@ -75,8 +53,6 @@ class WeatherScreenVM : ViewModel(), KoinComponent {
permissionsManager.requestPermission(activity, PermissionGroup.Location)
}
val isSearchingLocation = MutableLiveData(false)
val locationResults = MutableLiveData<List<WeatherLocation>>(emptyList())
init {
viewModelScope.launch {