diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt index 8028ba29..1c7c389d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack 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.Error 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.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -48,7 +50,9 @@ import coil.compose.AsyncImage import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.plugin.PluginState 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.preferences.Preference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.locals.LocalNavController @@ -76,6 +80,11 @@ fun PluginSettingsScreen(pluginId: String) { minActiveState = Lifecycle.State.RESUMED ) + val weatherPlugins by viewModel.weatherPlugins.collectAsStateWithLifecycle( + emptyList(), + minActiveState = Lifecycle.State.RESUMED + ) + val requestPermissionStarter = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { @@ -87,6 +96,10 @@ fun PluginSettingsScreen(pluginId: String) { null ) + val weatherProviderId by viewModel.weatherProvider.collectAsStateWithLifecycle( + null + ) + Scaffold( topBar = { TopAppBar( @@ -286,7 +299,7 @@ fun PluginSettingsScreen(pluginId: String) { AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) { if (filePlugins.isNotEmpty()) { PreferenceCategory( - "File search", + stringResource(R.string.plugin_type_filesearch), iconPadding = false, ) { for (plugin in filePlugins) { @@ -294,7 +307,7 @@ fun PluginSettingsScreen(pluginId: String) { if (state is PluginState.SetupRequired) { Banner( 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, primaryAction = { TextButton(onClick = { @@ -311,7 +324,7 @@ fun PluginSettingsScreen(pluginId: String) { } else if (state is PluginState.Error) { Banner( modifier = Modifier.padding(16.dp), - text = "This plugin isn't working correctly", + text = stringResource(R.string.plugin_state_error), icon = Icons.Rounded.Error, 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") + } + ) + } + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreenVM.kt index 036c3b44..c05483a5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreenVM.kt @@ -14,6 +14,7 @@ import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.plugin.PluginType import de.mm20.launcher2.plugins.PluginService import de.mm20.launcher2.plugins.PluginWithState +import de.mm20.launcher2.weather.settings.WeatherSettings import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -30,6 +31,7 @@ import org.koin.core.component.inject class PluginSettingsScreenVM : ViewModel(), KoinComponent { private val pluginService by inject() private val fileSearchSettings: FileSearchSettings by inject() + private val weatherSettings: WeatherSettings by inject() private var pluginPackageName = MutableStateFlow(null) @@ -70,6 +72,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent { it.filter { it.plugin.type == PluginType.FileSearch } } + val weatherPlugins = states + .map { + it.filter { it.plugin.type == PluginType.Weather } + } + fun init(pluginId: String) { this.pluginPackageName.value = pluginId @@ -101,4 +108,9 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent { fun setFileSearchPluginEnabled(authority: String, enabled: Boolean) { fileSearchSettings.setPluginEnabled(authority, enabled) } + + val weatherProvider = weatherSettings.providerId + fun setWeatherProvider(providerId: String) { + weatherSettings.setProviderId(providerId) + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt index ee787a05..dab574aa 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt @@ -1,18 +1,27 @@ package de.mm20.launcher2.ui.settings.weather +import android.app.PendingIntent import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility 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.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.R 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.preferences.* import de.mm20.launcher2.weather.WeatherLocation @@ -25,12 +34,36 @@ fun WeatherIntegrationSettingsScreen() { val availableProviders by viewModel.availableProviders.collectAsState(emptyList()) + val pluginState by viewModel.weatherProviderPluginState.collectAsStateWithLifecycle( + null, + minActiveState = Lifecycle.State.RESUMED + ) + PreferenceScreen( title = stringResource(R.string.preference_screen_weatherwidget), helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/integrations/weather" ) { item { 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() ListPreference( title = stringResource(R.string.preference_weather_provider), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt index 0e90ed46..32f28032 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.plugins.PluginService import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.weather.WeatherLocation import de.mm20.launcher2.weather.WeatherProviderInfo @@ -15,9 +16,11 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMap import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -25,6 +28,7 @@ import org.koin.core.component.inject class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { private val repository: WeatherRepository by inject() private val weatherSettings: WeatherSettings by inject() + private val pluginService: PluginService by inject() private val permissionsManager: PermissionsManager by inject() private val dataStore: LauncherDataStore by inject() @@ -36,6 +40,10 @@ class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { 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 } fun setImperialUnits(imperialUnits: Boolean) { viewModelScope.launch { diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 42c4e299..a4ec2551 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -852,4 +852,11 @@ Unavailable This shortcut is unavailable because %1$s isn\'t the default launcher + This plugin isn\'t working correctly + You need to setup this plugin first + Set up + File search + Weather provider + Set as weather provider + Currently set as weather provider \ No newline at end of file diff --git a/data/database/src/main/java/de/mm20/launcher2/database/daos/PluginDao.kt b/data/database/src/main/java/de/mm20/launcher2/database/daos/PluginDao.kt index 6e1923eb..68f483ef 100644 --- a/data/database/src/main/java/de/mm20/launcher2/database/daos/PluginDao.kt +++ b/data/database/src/main/java/de/mm20/launcher2/database/daos/PluginDao.kt @@ -25,7 +25,7 @@ interface PluginDao { ): Flow> @Query("SELECT * FROM Plugins WHERE authority = :authority") - fun get(authority: String): Flow + fun get(authority: String): Flow @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertMany(plugins: List) diff --git a/data/plugins/src/main/java/de/mm20/launcher2/data/plugins/PluginRepositoryImpl.kt b/data/plugins/src/main/java/de/mm20/launcher2/data/plugins/PluginRepositoryImpl.kt index 15cc8865..60444aa3 100644 --- a/data/plugins/src/main/java/de/mm20/launcher2/data/plugins/PluginRepositoryImpl.kt +++ b/data/plugins/src/main/java/de/mm20/launcher2/data/plugins/PluginRepositoryImpl.kt @@ -31,7 +31,7 @@ internal class PluginRepositoryImpl( } override fun get(authority: String): Flow { - return dao.get(authority).map { Plugin(it) } + return dao.get(authority).map { it?.let { Plugin(it) } } } override fun insertMany(plugins: List): Job { diff --git a/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginService.kt b/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginService.kt index 43b210f4..ed453333 100644 --- a/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginService.kt +++ b/services/plugins/src/main/java/de/mm20/launcher2/plugins/PluginService.kt @@ -46,6 +46,10 @@ interface PluginService { enabled: Boolean? = null, ): Flow> + fun getPluginWithState( + authority: String, + ): Flow + fun getPluginPackages(): Flow> fun getPluginPackage(packageName: String): Flow suspend fun getPluginState(plugin: Plugin): PluginState? @@ -132,6 +136,17 @@ internal class PluginServiceImpl( } } + override fun getPluginWithState(authority: String): Flow { + return repository.get(authority).map { + it?.let { + PluginWithState( + plugin = it, + state = getPluginState(it), + ) + } + } + } + override suspend fun getPluginState(plugin: Plugin): PluginState { val bundle = try {