From 61c77a137d6447c4df07462a798c53a717a3fc51 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Mon, 3 Jan 2022 23:45:33 +0100 Subject: [PATCH] Weather widget: add button to manually set location if permission is missing --- i18n/src/main/res/values-de/strings.xml | 1 + i18n/src/main/res/values/strings.xml | 1 + .../ui/common/WeatherLocationSearchDialog.kt | 116 ++++++++++++++++++ .../common/WeatherLocationSearchDialogVM.kt | 41 +++++++ .../launcher/widgets/weather/WeatherWidget.kt | 21 +++- .../ui/settings/weather/WeatherScreen.kt | 116 +----------------- .../ui/settings/weather/WeatherScreenVM.kt | 24 ---- 7 files changed, 181 insertions(+), 139 deletions(-) create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialog.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialogVM.kt diff --git a/i18n/src/main/res/values-de/strings.xml b/i18n/src/main/res/values-de/strings.xml index e6cfa307..ff18592e 100644 --- a/i18n/src/main/res/values-de/strings.xml +++ b/i18n/src/main/res/values-de/strings.xml @@ -423,4 +423,5 @@ Gewähren Standortzugriff wird benötigt, um den Standort automatisch zu ermitteln + Standort festlegen \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index b36cd965..3621d044 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -461,4 +461,5 @@ Grant Location access is required to determine the location automatically + Set location diff --git a/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialog.kt b/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialog.kt new file mode 100644 index 00000000..e7a14b6a --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialog.kt @@ -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 + ) + } + } + + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialogVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialogVM.kt new file mode 100644 index 00000000..9a3bb5a3 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialogVM.kt @@ -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>(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) + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt index a998364c..8d7daa94 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt @@ -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 diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherScreen.kt index 72144dfe..dcfc54b3 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherScreen.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherScreen.kt @@ -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, 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 }) } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherScreenVM.kt index 88109f3f..f7695c4a 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherScreenVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherScreenVM.kt @@ -46,28 +46,6 @@ class WeatherScreenVM : ViewModel(), KoinComponent { } val location = MutableLiveData(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>(emptyList()) init { viewModelScope.launch {