Add weather plugin specific settings

This commit is contained in:
MM20 2023-12-29 17:50:55 +01:00
parent f984f68b34
commit 35ff1bce1a
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 145 additions and 5 deletions

View File

@ -20,6 +20,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile
import androidx.compose.material.icons.automirrored.rounded.OpenInNew
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Error import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Info
@ -40,6 +41,7 @@ import androidx.compose.runtime.getValue
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -48,7 +50,9 @@ import coil.compose.AsyncImage
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalNavController import de.mm20.launcher2.ui.locals.LocalNavController
@ -76,6 +80,11 @@ fun PluginSettingsScreen(pluginId: String) {
minActiveState = Lifecycle.State.RESUMED minActiveState = Lifecycle.State.RESUMED
) )
val weatherPlugins by viewModel.weatherPlugins.collectAsStateWithLifecycle(
emptyList(),
minActiveState = Lifecycle.State.RESUMED
)
val requestPermissionStarter = val requestPermissionStarter =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) { if (it.resultCode == Activity.RESULT_OK) {
@ -87,6 +96,10 @@ fun PluginSettingsScreen(pluginId: String) {
null null
) )
val weatherProviderId by viewModel.weatherProvider.collectAsStateWithLifecycle(
null
)
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -286,7 +299,7 @@ fun PluginSettingsScreen(pluginId: String) {
AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) { AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) {
if (filePlugins.isNotEmpty()) { if (filePlugins.isNotEmpty()) {
PreferenceCategory( PreferenceCategory(
"File search", stringResource(R.string.plugin_type_filesearch),
iconPadding = false, iconPadding = false,
) { ) {
for (plugin in filePlugins) { for (plugin in filePlugins) {
@ -294,7 +307,7 @@ fun PluginSettingsScreen(pluginId: String) {
if (state is PluginState.SetupRequired) { if (state is PluginState.SetupRequired) {
Banner( Banner(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
text = state.message ?: "You need to setup this plugin first", text = state.message ?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info, icon = Icons.Rounded.Info,
primaryAction = { primaryAction = {
TextButton(onClick = { TextButton(onClick = {
@ -311,7 +324,7 @@ fun PluginSettingsScreen(pluginId: String) {
} else if (state is PluginState.Error) { } else if (state is PluginState.Error) {
Banner( Banner(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
text = "This plugin isn't working correctly", text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error, icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer, color = MaterialTheme.colorScheme.errorContainer,
) )
@ -334,6 +347,58 @@ fun PluginSettingsScreen(pluginId: String) {
} }
} }
} }
if (weatherPlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_weather),
iconPadding = false,
) {
for (plugin in weatherPlugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.send()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}
)
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
)
}
Preference(
title = plugin.plugin.label,
enabled = state is PluginState.Ready && weatherProviderId != plugin.plugin.authority,
iconPadding = false,
summary = if (weatherProviderId != plugin.plugin.authority) {
stringResource(R.string.plugin_weather_provider_enable)
} else {
stringResource(R.string.plugin_weather_provider_enabled)
}
)
}
Preference(
title = stringResource(R.string.widget_config_weather_integration_settings),
icon = Icons.AutoMirrored.Rounded.OpenInNew,
onClick = {
navController?.navigate("settings/integrations/weather")
}
)
}
}
} }
} }
} }

View File

@ -14,6 +14,7 @@ import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.plugins.PluginWithState import de.mm20.launcher2.plugins.PluginWithState
import de.mm20.launcher2.weather.settings.WeatherSettings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@ -30,6 +31,7 @@ import org.koin.core.component.inject
class PluginSettingsScreenVM : ViewModel(), KoinComponent { class PluginSettingsScreenVM : ViewModel(), KoinComponent {
private val pluginService by inject<PluginService>() private val pluginService by inject<PluginService>()
private val fileSearchSettings: FileSearchSettings by inject() private val fileSearchSettings: FileSearchSettings by inject()
private val weatherSettings: WeatherSettings by inject()
private var pluginPackageName = MutableStateFlow<String?>(null) private var pluginPackageName = MutableStateFlow<String?>(null)
@ -70,6 +72,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
it.filter { it.plugin.type == PluginType.FileSearch } it.filter { it.plugin.type == PluginType.FileSearch }
} }
val weatherPlugins = states
.map {
it.filter { it.plugin.type == PluginType.Weather }
}
fun init(pluginId: String) { fun init(pluginId: String) {
this.pluginPackageName.value = pluginId this.pluginPackageName.value = pluginId
@ -101,4 +108,9 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
fun setFileSearchPluginEnabled(authority: String, enabled: Boolean) { fun setFileSearchPluginEnabled(authority: String, enabled: Boolean) {
fileSearchSettings.setPluginEnabled(authority, enabled) fileSearchSettings.setPluginEnabled(authority, enabled)
} }
val weatherProvider = weatherSettings.providerId
fun setWeatherProvider(providerId: String) {
weatherSettings.setProviderId(providerId)
}
} }

View File

@ -1,18 +1,27 @@
package de.mm20.launcher2.ui.settings.weather package de.mm20.launcher2.ui.settings.weather
import android.app.PendingIntent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
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.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.ui.BuildConfig import de.mm20.launcher2.ui.BuildConfig
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.preferences.* import de.mm20.launcher2.ui.component.preferences.*
import de.mm20.launcher2.weather.WeatherLocation import de.mm20.launcher2.weather.WeatherLocation
@ -25,12 +34,36 @@ fun WeatherIntegrationSettingsScreen() {
val availableProviders by viewModel.availableProviders.collectAsState(emptyList()) val availableProviders by viewModel.availableProviders.collectAsState(emptyList())
val pluginState by viewModel.weatherProviderPluginState.collectAsStateWithLifecycle(
null,
minActiveState = Lifecycle.State.RESUMED
)
PreferenceScreen( PreferenceScreen(
title = stringResource(R.string.preference_screen_weatherwidget), title = stringResource(R.string.preference_screen_weatherwidget),
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/integrations/weather" helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/integrations/weather"
) { ) {
item { item {
PreferenceCategory { PreferenceCategory {
val state = pluginState?.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.send()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}
)
}
val weatherProvider by viewModel.weatherProvider.collectAsState() val weatherProvider by viewModel.weatherProvider.collectAsState()
ListPreference( ListPreference(
title = stringResource(R.string.preference_weather_provider), title = stringResource(R.string.preference_weather_provider),

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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.plugins.PluginService
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.weather.WeatherLocation import de.mm20.launcher2.weather.WeatherLocation
import de.mm20.launcher2.weather.WeatherProviderInfo import de.mm20.launcher2.weather.WeatherProviderInfo
@ -15,9 +16,11 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMap
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -25,6 +28,7 @@ import org.koin.core.component.inject
class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
private val repository: WeatherRepository by inject() private val repository: WeatherRepository by inject()
private val weatherSettings: WeatherSettings by inject() private val weatherSettings: WeatherSettings by inject()
private val pluginService: PluginService by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
@ -36,6 +40,10 @@ class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
weatherSettings.setProviderId(provider) weatherSettings.setProviderId(provider)
} }
val weatherProviderPluginState = weatherProvider.flatMapLatest {
it?.let { pluginService.getPluginWithState(it) } ?: flowOf(null)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val imperialUnits = dataStore.data.map { it.weather.imperialUnits } val imperialUnits = dataStore.data.map { it.weather.imperialUnits }
fun setImperialUnits(imperialUnits: Boolean) { fun setImperialUnits(imperialUnits: Boolean) {
viewModelScope.launch { viewModelScope.launch {

View File

@ -852,4 +852,11 @@
<string name="shortcut_label_unavailable">Unavailable</string> <string name="shortcut_label_unavailable">Unavailable</string>
<!-- %1$s: app name --> <!-- %1$s: app name -->
<string name="shortcut_unavailable_description">This shortcut is unavailable because %1$s isn\'t the default launcher</string> <string name="shortcut_unavailable_description">This shortcut is unavailable because %1$s isn\'t the default launcher</string>
<string name="plugin_state_error">This plugin isn\'t working correctly</string>
<string name="plugin_state_setup_required">You need to setup this plugin first</string>
<string name="plugin_action_setup">Set up</string>
<string name="plugin_type_filesearch">File search</string>
<string name="plugin_type_weather">Weather provider</string>
<string name="plugin_weather_provider_enable">Set as weather provider</string>
<string name="plugin_weather_provider_enabled">Currently set as weather provider</string>
</resources> </resources>

View File

@ -25,7 +25,7 @@ interface PluginDao {
): Flow<List<PluginEntity>> ): Flow<List<PluginEntity>>
@Query("SELECT * FROM Plugins WHERE authority = :authority") @Query("SELECT * FROM Plugins WHERE authority = :authority")
fun get(authority: String): Flow<PluginEntity> fun get(authority: String): Flow<PluginEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMany(plugins: List<PluginEntity>) suspend fun insertMany(plugins: List<PluginEntity>)

View File

@ -31,7 +31,7 @@ internal class PluginRepositoryImpl(
} }
override fun get(authority: String): Flow<Plugin?> { override fun get(authority: String): Flow<Plugin?> {
return dao.get(authority).map { Plugin(it) } return dao.get(authority).map { it?.let { Plugin(it) } }
} }
override fun insertMany(plugins: List<Plugin>): Job { override fun insertMany(plugins: List<Plugin>): Job {

View File

@ -46,6 +46,10 @@ interface PluginService {
enabled: Boolean? = null, enabled: Boolean? = null,
): Flow<List<PluginWithState>> ): Flow<List<PluginWithState>>
fun getPluginWithState(
authority: String,
): Flow<PluginWithState?>
fun getPluginPackages(): Flow<List<PluginPackage>> fun getPluginPackages(): Flow<List<PluginPackage>>
fun getPluginPackage(packageName: String): Flow<PluginPackage?> fun getPluginPackage(packageName: String): Flow<PluginPackage?>
suspend fun getPluginState(plugin: Plugin): PluginState? suspend fun getPluginState(plugin: Plugin): PluginState?
@ -132,6 +136,17 @@ internal class PluginServiceImpl(
} }
} }
override fun getPluginWithState(authority: String): Flow<PluginWithState?> {
return repository.get(authority).map {
it?.let {
PluginWithState(
plugin = it,
state = getPluginState(it),
)
}
}
}
override suspend fun getPluginState(plugin: Plugin): PluginState { override suspend fun getPluginState(plugin: Plugin): PluginState {
val bundle = val bundle =
try { try {