From 6b51b5ca20b517fbab2cd5f7f7f00e6af5b6e8b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Tue, 3 Dec 2024 13:32:44 +0000 Subject: [PATCH] Select/deselect all website categories --- .../composeResources/drawable/ic_deselect.xml | 10 ++ .../drawable/ic_select_all.xml | 10 ++ .../values/strings-common.xml | 2 + .../kotlin/org/ooni/probe/di/Dependencies.kt | 20 ++-- .../org/ooni/probe/domain/GetSettings.kt | 9 -- .../ooni/probe/ui/navigation/Navigation.kt | 9 ++ .../webcategories/WebCategoriesScreen.kt | 95 ++++++++++++++++++ .../webcategories/WebCategoriesViewModel.kt | 97 +++++++++++++++++++ 8 files changed, 237 insertions(+), 15 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_deselect.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_select_all.xml create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/webcategories/WebCategoriesScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/webcategories/WebCategoriesViewModel.kt diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_deselect.xml b/composeApp/src/commonMain/composeResources/drawable/ic_deselect.xml new file mode 100644 index 00000000..d091588e --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_deselect.xml @@ -0,0 +1,10 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_select_all.xml b/composeApp/src/commonMain/composeResources/drawable/ic_select_all.xml new file mode 100644 index 00000000..363adc4e --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_select_all.xml @@ -0,0 +1,10 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml index da685069..781bf8e1 100644 --- a/composeApp/src/commonMain/composeResources/values/strings-common.xml +++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml @@ -237,6 +237,8 @@ refresh Collapse Expand + Select all + Deselect all %1$s ago %1$d minute %1$d minutes diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt index 1e5a9952..660b3541 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -92,6 +92,7 @@ import org.ooni.probe.ui.settings.SettingsViewModel import org.ooni.probe.ui.settings.about.AboutViewModel import org.ooni.probe.ui.settings.category.SettingsCategoryViewModel import org.ooni.probe.ui.settings.proxy.ProxyViewModel +import org.ooni.probe.ui.settings.webcategories.WebCategoriesViewModel import org.ooni.probe.ui.upload.UploadMeasurementsViewModel import kotlin.coroutines.CoroutineContext @@ -530,6 +531,15 @@ class Dependencies( shareUrl = { launchAction(PlatformAction.Share(it)) }, ) + fun reviewUpdatesViewModel(onBack: () -> Unit): ReviewUpdatesViewModel { + return ReviewUpdatesViewModel( + onBack = onBack, + createOrUpdate = testDescriptorRepository::createOrUpdate, + cancelUpdates = getDescriptorUpdate::cancelUpdates, + observeAvailableUpdatesState = getDescriptorUpdate::observeAvailableUpdatesState, + ) + } + fun settingsCategoryViewModel( categoryKey: String, goToSettingsForCategory: (PreferenceCategoryKey) -> Unit, @@ -558,14 +568,12 @@ class Dependencies( uploadMissingMeasurements = uploadMissingMeasurements::invoke, ) - fun reviewUpdatesViewModel(onBack: () -> Unit): ReviewUpdatesViewModel { - return ReviewUpdatesViewModel( + fun webCategoriesViewModel(onBack: () -> Unit) = + WebCategoriesViewModel( onBack = onBack, - createOrUpdate = testDescriptorRepository::createOrUpdate, - cancelUpdates = getDescriptorUpdate::cancelUpdates, - observeAvailableUpdatesState = getDescriptorUpdate::observeAvailableUpdatesState, + getPreferencesByKeys = preferenceRepository::allSettings, + setPreferenceValuesByKeys = preferenceRepository::setValuesByKey, ) - } companion object { @VisibleForTesting diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetSettings.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetSettings.kt index b03b1f9b..aa46122d 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetSettings.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetSettings.kt @@ -240,15 +240,6 @@ class GetSettings( ), ) }, - settings = WebConnectivityCategory.entries.mapNotNull { cat -> - SettingsItem( - icon = cat.icon, - title = cat.title, - supportingContent = { Text(stringResource(cat.description)) }, - key = cat.settingsKey ?: return@mapNotNull null, - type = PreferenceItemType.SWITCH, - ) - }, ), ) } else { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt index f39b19c6..bf7abd36 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt @@ -39,6 +39,7 @@ import org.ooni.probe.ui.settings.SettingsScreen import org.ooni.probe.ui.settings.about.AboutScreen import org.ooni.probe.ui.settings.category.SettingsCategoryScreen import org.ooni.probe.ui.settings.proxy.ProxyScreen +import org.ooni.probe.ui.settings.webcategories.WebCategoriesScreen import org.ooni.probe.ui.upload.UploadMeasurementsDialog private val START_SCREEN = Screen.Dashboard @@ -165,6 +166,14 @@ fun Navigation( entry.arguments?.getString("category"), ) ?: return@composable when (category) { + PreferenceCategoryKey.WEBSITES_CATEGORIES -> { + val viewModel = viewModel { + dependencies.webCategoriesViewModel(onBack = { navController.goBack() }) + } + val state by viewModel.state.collectAsState() + WebCategoriesScreen(state, viewModel::onEvent) + } + PreferenceCategoryKey.ABOUT_OONI -> { val viewModel = viewModel { dependencies.aboutViewModel(onBack = { navController.goBack() }) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/webcategories/WebCategoriesScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/webcategories/WebCategoriesScreen.kt new file mode 100644 index 00000000..f63515cf --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/webcategories/WebCategoriesScreen.kt @@ -0,0 +1,95 @@ +package org.ooni.probe.ui.settings.webcategories + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp +import ooniprobe.composeapp.generated.resources.Common_Back +import ooniprobe.composeapp.generated.resources.Common_DeselectAll +import ooniprobe.composeapp.generated.resources.Common_SelectAll +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Settings_Websites_Categories_Label +import ooniprobe.composeapp.generated.resources.ic_deselect +import ooniprobe.composeapp.generated.resources.ic_select_all +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.ui.settings.category.SwitchSettingsView +import org.ooni.probe.ui.shared.TopBar + +@Composable +fun WebCategoriesScreen( + state: WebCategoriesViewModel.State, + onEvent: (WebCategoriesViewModel.Event) -> Unit, +) { + Column(Modifier.background(MaterialTheme.colorScheme.background)) { + TopBar( + title = { + Text( + stringResource(Res.string.Settings_Websites_Categories_Label), + style = MaterialTheme.typography.headlineSmall.copy(fontSize = 18.sp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton(onClick = { onEvent(WebCategoriesViewModel.Event.BackClicked) }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.Common_Back), + ) + } + }, + actions = { + IconButton( + onClick = { onEvent(WebCategoriesViewModel.Event.SelectAllClicked) }, + enabled = state.selectAllEnabled, + ) { + Icon( + painterResource(Res.drawable.ic_select_all), + stringResource(Res.string.Common_SelectAll), + ) + } + IconButton( + onClick = { onEvent(WebCategoriesViewModel.Event.DeselectAllClicked) }, + enabled = state.deselectAllEnabled, + ) { + Icon( + painterResource(Res.drawable.ic_deselect), + stringResource(Res.string.Common_DeselectAll), + ) + } + }, + ) + + LazyColumn( + contentPadding = WindowInsets.navigationBars.asPaddingValues(), + ) { + items(state.items, key = { it.item.settingsKey!! }) { item -> + SwitchSettingsView( + icon = item.item.icon, + title = item.item.title, + key = item.item.settingsKey!!, + checked = item.isSelected, + enabled = true, + supportingContent = { Text(stringResource(item.item.description)) }, + onCheckedChange = { _, value -> + onEvent(WebCategoriesViewModel.Event.PreferenceChanged(item.item, value)) + }, + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/webcategories/WebCategoriesViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/webcategories/WebCategoriesViewModel.kt new file mode 100644 index 00000000..49e04479 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/webcategories/WebCategoriesViewModel.kt @@ -0,0 +1,97 @@ +package org.ooni.probe.ui.settings.webcategories + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import org.ooni.engine.models.WebConnectivityCategory +import org.ooni.probe.data.models.SettingsKey +import org.ooni.probe.ui.shared.SelectableItem + +class WebCategoriesViewModel( + onBack: () -> Unit, + getPreferencesByKeys: (List) -> Flow>, + setPreferenceValuesByKeys: suspend (List>) -> Unit, +) : ViewModel() { + private val events = MutableSharedFlow(extraBufferCapacity = 1) + + private val _state = MutableStateFlow(State()) + val state = _state.asStateFlow() + + init { + val categories = WebConnectivityCategory.entries.filter { it.settingsKey != null } + + getPreferencesByKeys(categories.mapNotNull { it.settingsKey }) + .onEach { preferences -> + _state.update { + it.copy( + items = categories.map { category -> + SelectableItem( + item = category, + isSelected = preferences[category.settingsKey] == true, + ) + }, + ) + } + } + .launchIn(viewModelScope) + + events.filterIsInstance() + .onEach { + setPreferenceValuesByKeys( + listOf((it.category.settingsKey ?: return@onEach) to it.value), + ) + } + .launchIn(viewModelScope) + + events.filterIsInstance() + .onEach { + setPreferenceValuesByKeys( + categories.map { (it.settingsKey ?: return@onEach) to true }, + ) + } + .launchIn(viewModelScope) + + events.filterIsInstance() + .onEach { + setPreferenceValuesByKeys( + categories.map { (it.settingsKey ?: return@onEach) to false }, + ) + } + .launchIn(viewModelScope) + + events.filterIsInstance() + .onEach { onBack() } + .launchIn(viewModelScope) + } + + fun onEvent(event: Event) { + events.tryEmit(event) + } + + data class State( + val items: List> = emptyList(), + ) { + val selectAllEnabled get() = items.any { !it.isSelected } + val deselectAllEnabled get() = items.any { it.isSelected } + } + + sealed interface Event { + data class PreferenceChanged( + val category: WebConnectivityCategory, + val value: Boolean, + ) : Event + + data object SelectAllClicked : Event + + data object DeselectAllClicked : Event + + data object BackClicked : Event + } +}