Weather widget: add warning if selected providerr is unavailable

This commit is contained in:
MM20 2024-06-30 15:19:09 +02:00
parent e96a382b00
commit 845a11b54b
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
6 changed files with 98 additions and 18 deletions

View File

@ -52,7 +52,7 @@
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:exported="true" android:exported="true"
android:label="@string/settings" android:label="@string/settings"
android:launchMode="singleTask" android:launchMode="singleTop"
android:parentActivityName=".launcher.SharedLauncherActivity" android:parentActivityName=".launcher.SharedLauncherActivity"
android:taskAffinity="de.mm20.launcher2.settings" android:taskAffinity="de.mm20.launcher2.settings"
android:theme="@style/SettingsTheme.NoActionBar" android:theme="@style/SettingsTheme.NoActionBar"

View File

@ -23,7 +23,15 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.automirrored.rounded.OpenInNew
import androidx.compose.material.icons.rounded.Air
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.LocationCity
import androidx.compose.material.icons.rounded.MyLocation
import androidx.compose.material.icons.rounded.North
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -42,21 +50,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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 androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.HumidityPercentage
import de.mm20.launcher2.icons.Rain
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.WeatherLocationSearchDialog import de.mm20.launcher2.ui.common.WeatherLocationSearchDialog
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.weather.AnimatedWeatherIcon import de.mm20.launcher2.ui.component.weather.AnimatedWeatherIcon
import de.mm20.launcher2.ui.component.weather.WeatherIcon import de.mm20.launcher2.ui.component.weather.WeatherIcon
import de.mm20.launcher2.icons.HumidityPercentage
import de.mm20.launcher2.icons.Rain
import de.mm20.launcher2.ui.ktx.blendIntoViewScale import de.mm20.launcher2.ui.ktx.blendIntoViewScale
import de.mm20.launcher2.ui.locals.LocalCardStyle import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.modifier.consumeAllScrolling import de.mm20.launcher2.ui.modifier.consumeAllScrolling
@ -86,16 +96,42 @@ fun WeatherWidget(widget: WeatherWidget) {
val imperialUnits by viewModel.imperialUnits.collectAsState(false) val imperialUnits by viewModel.imperialUnits.collectAsState(false)
val compactMode = !widget.config.showForecast val compactMode = !widget.config.showForecast
val isProviderAvailable by viewModel.isProviderAvailable.collectAsStateWithLifecycle(true)
var showLocationDialog by remember { mutableStateOf(false) } var showLocationDialog by remember { mutableStateOf(false) }
if (showLocationDialog) { if (showLocationDialog) {
WeatherLocationSearchDialog(onDismissRequest = { showLocationDialog = false }) WeatherLocationSearchDialog(onDismissRequest = { showLocationDialog = false })
} }
val forecast = selectedForecast ?: run {
val hasPermission by viewModel.hasLocationPermission.collectAsState() Column {
val autoLocation by viewModel.autoLocation.collectAsState() if (!isProviderAvailable) {
Column { Banner(
modifier = Modifier.fillMaxWidth().padding(16.dp),
text = stringResource(R.string.weather_widget_no_provider),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
Button(
onClick = {
viewModel.openSettings(context)
},
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
Icons.AutoMirrored.Rounded.OpenInNew,
null,
modifier = Modifier.padding(end = ButtonDefaults.IconSpacing).size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.settings))
}
}
)
}
val forecast = selectedForecast ?: run {
val hasPermission by viewModel.hasLocationPermission.collectAsState()
val autoLocation by viewModel.autoLocation.collectAsState()
AnimatedVisibility(hasPermission == false && autoLocation == true) { AnimatedVisibility(hasPermission == false && autoLocation == true) {
MissingPermissionBanner( MissingPermissionBanner(
modifier = Modifier modifier = Modifier
@ -117,11 +153,10 @@ fun WeatherWidget(widget: WeatherWidget) {
) )
} }
NoData() NoData()
return
} }
return
}
Column {
CurrentWeather(forecast, imperialUnits) CurrentWeather(forecast, imperialUnits)
if (!compactMode) { if (!compactMode) {
@ -339,7 +374,9 @@ fun WeatherTimeSelector(
val listState = rememberLazyListState() val listState = rememberLazyListState()
LazyRow( LazyRow(
state = listState, state = listState,
modifier = modifier.fillMaxWidth().consumeAllScrolling(), modifier = modifier
.fillMaxWidth()
.consumeAllScrolling(),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(start = 16.dp, end = 16.dp), contentPadding = PaddingValues(start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -398,7 +435,9 @@ fun WeatherDaySelector(
val listState = rememberLazyListState() val listState = rememberLazyListState()
LazyRow( LazyRow(
state = listState, state = listState,
modifier = modifier.fillMaxWidth().consumeAllScrolling(), modifier = modifier
.fillMaxWidth()
.consumeAllScrolling(),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(start = 16.dp, end = 16.dp), contentPadding = PaddingValues(start = 16.dp, end = 16.dp),

View File

@ -1,14 +1,18 @@
package de.mm20.launcher2.ui.launcher.widgets.weather package de.mm20.launcher2.ui.launcher.widgets.weather
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.* import androidx.lifecycle.*
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.weather.WeatherSettings import de.mm20.launcher2.preferences.weather.WeatherSettings
import de.mm20.launcher2.ui.settings.SettingsActivity
import de.mm20.launcher2.weather.DailyForecast import de.mm20.launcher2.weather.DailyForecast
import de.mm20.launcher2.weather.Forecast import de.mm20.launcher2.weather.Forecast
import de.mm20.launcher2.weather.WeatherRepository import de.mm20.launcher2.weather.WeatherRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -132,4 +136,16 @@ class WeatherWidgetVM : ViewModel(), KoinComponent {
selectDay(dayIndex) selectDay(dayIndex)
selectForecast(forecastIndex) selectForecast(forecastIndex)
} }
fun openSettings(context: Context) {
context.startActivity(
Intent(context, SettingsActivity::class.java).apply {
putExtra(SettingsActivity.EXTRA_ROUTE, SettingsActivity.ROUTE_WEATHER_INTEGRATION)
}
)
}
val isProviderAvailable: Flow<Boolean> = weatherRepository.getActiveProvider().map {
it != null
}
} }

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.settings package de.mm20.launcher2.ui.settings
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -14,7 +15,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@ -72,16 +75,22 @@ import java.util.UUID
class SettingsActivity : BaseActivity() { class SettingsActivity : BaseActivity() {
private var route by mutableStateOf<String?>(null)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val newRoute = intent?.getStringExtra(EXTRA_ROUTE)
route = newRoute
setContent { setContent {
val navController = rememberNavController() val navController = rememberNavController()
LaunchedEffect(intent) { LaunchedEffect(route) {
intent.getStringExtra(EXTRA_ROUTE) navController.navigate(route ?: "settings") {
?.let { navController.navigate(it) } popUpTo("settings")
}
} }
val wallpaperColors by wallpaperColorsAsState() val wallpaperColors by wallpaperColorsAsState()
CompositionLocalProvider( CompositionLocalProvider(
@ -107,7 +116,7 @@ class SettingsActivity : BaseActivity() {
navController = navController, navController = navController,
startDestination = "settings", startDestination = "settings",
exitTransition = { exitTransition = {
slideOutHorizontally {-it / 4 } slideOutHorizontally { -it / 4 }
}, },
enterTransition = { enterTransition = {
slideInHorizontally { it / 2 } + scaleIn(initialScale = 0.9f) + fadeIn() slideInHorizontally { it / 2 } + scaleIn(initialScale = 0.9f) + fadeIn()
@ -267,6 +276,12 @@ class SettingsActivity : BaseActivity() {
} }
} }
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val newRoute = intent.getStringExtra(EXTRA_ROUTE)
route = newRoute
}
companion object { companion object {
const val EXTRA_ROUTE = "de.mm20.launcher2.settings.ROUTE" const val EXTRA_ROUTE = "de.mm20.launcher2.settings.ROUTE"
const val ROUTE_WEATHER_INTEGRATION = "settings/integrations/weather" const val ROUTE_WEATHER_INTEGRATION = "settings/integrations/weather"

View File

@ -968,4 +968,5 @@
<item quantity="one">%1$s notification</item> <item quantity="one">%1$s notification</item>
<item quantity="other">%1$s notifications</item> <item quantity="other">%1$s notifications</item>
</plurals> </plurals>
<string name="weather_widget_no_provider">No weather provider selected or the selected provider is not available</string>
</resources> </resources>

View File

@ -27,6 +27,7 @@ import java.util.*
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
interface WeatherRepository { interface WeatherRepository {
fun getActiveProvider(): Flow<WeatherProviderInfo?>
fun getProviders(): Flow<List<WeatherProviderInfo>> fun getProviders(): Flow<List<WeatherProviderInfo>>
fun searchLocations(query: String): Flow<List<WeatherLocation>> fun searchLocations(query: String): Flow<List<WeatherLocation>>
@ -147,6 +148,14 @@ internal class WeatherRepositoryImpl(
} }
} }
override fun getActiveProvider(): Flow<WeatherProviderInfo?> {
return settings.providerId.flatMapLatest { id ->
getProviders().map {
it.find { it.id == id }
}
}
}
override fun getProviders(): Flow<List<WeatherProviderInfo>> { override fun getProviders(): Flow<List<WeatherProviderInfo>> {
val providers = mutableListOf<WeatherProviderInfo>() val providers = mutableListOf<WeatherProviderInfo>()
providers.add( providers.add(