From 6941d6bce6c2a0d1cfcdda6690bab0993d355814 Mon Sep 17 00:00:00 2001 From: Maciej Procyk Date: Sat, 14 Oct 2023 21:27:53 +0200 Subject: [PATCH] add grouping of data when sorting is enabled --- .../component/AddTotpProviderComponent.kt | 2 +- .../kotlin/openotp/component/MainComponent.kt | 49 ++++++--- .../openotp/component/ScanQRCodeComponent.kt | 2 +- .../openotp/component/SettingsComponent.kt | 10 ++ .../openotp/component/UserPreferencesModel.kt | 12 ++- .../ml/dev/kotlin/openotp/otp/TotpData.kt | 5 +- .../openotp/ui/component/DragDropList.kt | 99 ++++++++++++++++--- .../ui/component/FilteredOtpCodeItems.kt | 10 +- .../openotp/ui/component/OtpCodeItems.kt | 6 +- .../kotlin/openotp/ui/screen/MainScreen.kt | 10 +- .../openotp/ui/screen/SettingsScreen.kt | 52 +++++++--- .../commonMain/resources/MR/base/strings.xml | 4 + .../commonMain/resources/MR/pl/strings.xml | 4 + 13 files changed, 211 insertions(+), 54 deletions(-) diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/AddTotpProviderComponent.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/AddTotpProviderComponent.kt index 4bd5f3d..022849d 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/AddTotpProviderComponent.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/AddTotpProviderComponent.kt @@ -51,7 +51,7 @@ abstract class AddOtpProviderComponentImpl( private val navigateOnCancelClicked: () -> Unit, ) : AbstractComponent(componentContext), AddOtpProviderComponent { - protected val secureStorage: StateFlowSettings = get(USER_OTP_CODE_DATA_MODULE_QUALIFIER) + protected val secureStorage: StateFlowSettings = get(USER_OTP_CODE_DATA_MODULE_QUALIFIER) protected fun notifyInvalid(fieldName: String) { toast(message = stringResource(OpenOtpResources.strings.invalid_field_name_provided_formatted, fieldName)) diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/MainComponent.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/MainComponent.kt index 90fed0f..b7e63a6 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/MainComponent.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/MainComponent.kt @@ -15,11 +15,11 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import ml.dev.kotlin.openotp.USER_OTP_CODE_DATA_MODULE_QUALIFIER import ml.dev.kotlin.openotp.USER_PREFERENCES_MODULE_QUALIFIER -import ml.dev.kotlin.openotp.otp.HotpData -import ml.dev.kotlin.openotp.otp.OtpData -import ml.dev.kotlin.openotp.otp.TotpData -import ml.dev.kotlin.openotp.otp.UserOtpCodeData +import ml.dev.kotlin.openotp.otp.* import ml.dev.kotlin.openotp.shared.OpenOtpResources +import ml.dev.kotlin.openotp.ui.component.DragDropListData +import ml.dev.kotlin.openotp.ui.component.DragDropListData.Grouped +import ml.dev.kotlin.openotp.ui.component.DragDropListData.Listed import ml.dev.kotlin.openotp.util.StateFlowSettings import ml.dev.kotlin.openotp.util.currentEpochMilliseconds import ml.dev.kotlin.openotp.util.unit @@ -31,9 +31,10 @@ interface MainComponent { val timestamp: Value val confirmOtpDataDelete: Value - val codeData: Value + val codeData: Value val isSearchActive: Value val isDragAndDropEnabled: Value + val showSortedGroupsHeaders: Value val navigateToScanQRCodeWhenCameraPermissionChanged: Value fun onRequestedCameraPermission() @@ -68,7 +69,9 @@ class MainComponentImpl( val changed: Boolean = false, ) - private val userOtpCodeData: StateFlowSettings = get(USER_OTP_CODE_DATA_MODULE_QUALIFIER) + private val appContext: OpenOtpAppComponentContext = get() + + private val userOtpCodeData: StateFlowSettings = get(USER_OTP_CODE_DATA_MODULE_QUALIFIER) private val userPreferences: StateFlowSettings = get(USER_PREFERENCES_MODULE_QUALIFIER) @@ -84,17 +87,13 @@ class MainComponentImpl( override val navigateToScanQRCodeWhenCameraPermissionChanged: Value = _cameraPermissionRequest.map { it.changed }.asValue() - override val codeData: Value = combine( + override val codeData: Value = combine( userOtpCodeData.stateFlow, userPreferences.stateFlow.map { it.sortOtpDataBy }, userPreferences.stateFlow.map { it.sortOtpDataReversed }, userPreferences.stateFlow.map { it.sortOtpDataNullsFirst }, - ) transform@{ codes, sortBy, sortReversed, sortNullsFirst -> - val selector = sortBy.selector ?: return@transform codes - val comparator: Comparator = if (sortNullsFirst) nullsFirst() else nullsLast() - val otpDataComparator: Comparator = compareBy(comparator) { selector(it)?.toLowerCase(Locale.current) } - val reversedComparator = if (sortReversed) otpDataComparator.reversed() else otpDataComparator - codes.sortedWith(reversedComparator) + ) { codes, sortBy, sortReversed, sortNullsFirst -> + appContext.sortOtpCodeDataWithRules(codes, sortBy, sortReversed, sortNullsFirst) }.asValue() private val _isSearchActive: MutableStateFlow = MutableStateFlow(false) @@ -103,6 +102,9 @@ class MainComponentImpl( override val isDragAndDropEnabled: Value = userPreferences.stateFlow.map { it.sortOtpDataBy == SortOtpDataBy.Dont }.asValue() + override val showSortedGroupsHeaders: Value = + userPreferences.stateFlow.map { it.showSortedGroupsHeaders }.asValue() + private val searchBackCallback: BackCallback = BackCallback { _isSearchActive.value = false } @@ -181,3 +183,24 @@ class MainComponentImpl( } private const val MAX_CAMERA_PERMISSION_SILENT_REQUESTS: Int = 2 + +private fun OpenOtpAppComponentContext.sortOtpCodeDataWithRules( + codes: StoredOtpCodeData, + sortBy: SortOtpDataBy, + sortReversed: Boolean, + sortNullsFirst: Boolean, +): DragDropListData { + val selector = sortBy.selector ?: return Listed(codes) + val comparator: Comparator = if (sortNullsFirst) nullsFirst() else nullsLast() + val otpDataComparator: Comparator = compareBy(comparator) { selector(it)?.toLowerCase(Locale.current) } + val reversedComparator = if (sortReversed) otpDataComparator.reversed() else otpDataComparator + val sorted = codes.sortedWith(reversedComparator) + + val groupedCodes = linkedMapOf>() + for (otpData in sorted) { + val groupName = selector(otpData) ?: with(sortBy) { defaultGroupName } + groupedCodes[groupName] ?: ArrayList().also { groupedCodes[groupName] = it } += otpData + } + val groups = groupedCodes.map { Grouped.Group(it.key, it.value) } + return Grouped(groups) +} diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/ScanQRCodeComponent.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/ScanQRCodeComponent.kt index 9ab3d0b..a141927 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/ScanQRCodeComponent.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/ScanQRCodeComponent.kt @@ -25,7 +25,7 @@ class ScanQRCodeComponentImpl( private val navigateOnCancel: (message: String?) -> Unit, ) : AbstractComponent(componentContext), ScanQRCodeComponent { - private val userOtpCodeData: StateFlowSettings = get(USER_OTP_CODE_DATA_MODULE_QUALIFIER) + private val userOtpCodeData: StateFlowSettings = get(USER_OTP_CODE_DATA_MODULE_QUALIFIER) override fun onQRCodeScanned(result: QRResult): Boolean = when (result) { is QRResult.QRError -> navigateOnCancel(invalidQRCodeMessage).letFalse() diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/SettingsComponent.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/SettingsComponent.kt index 1d151e6..8e6bbb8 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/SettingsComponent.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/SettingsComponent.kt @@ -15,6 +15,7 @@ interface SettingsComponent { val canReorderDataManually: Value val sortOtpDataNullsFirst: Value val sortOtpDataReversed: Value + val showSortedGroupsHeaders: Value val requireAuthentication: Value val isAuthenticationAvailable: Boolean @@ -28,6 +29,8 @@ interface SettingsComponent { fun onSortReversedChange(reversed: Boolean) + fun onShowSortedGroupsHeadersChange(show: Boolean) + fun onRequireAuthenticationChange(require: Boolean) fun onExitSettings() @@ -60,6 +63,9 @@ class SettingsComponentImpl( override val sortOtpDataReversed: Value = userPreferences.stateFlow.map { it.sortOtpDataReversed }.asValue() + override val showSortedGroupsHeaders: Value = + userPreferences.stateFlow.map { it.showSortedGroupsHeaders }.asValue() + override val requireAuthentication: Value = userPreferences.stateFlow.map { it.requireAuthentication }.asValue() @@ -86,6 +92,10 @@ class SettingsComponentImpl( userPreferences.updateInScope { it.copy(sortOtpDataReversed = reversed) } } + override fun onShowSortedGroupsHeadersChange(show: Boolean) { + userPreferences.updateInScope { it.copy(showSortedGroupsHeaders = show) } + } + override fun onRequireAuthenticationChange(require: Boolean) { userPreferences.updateInScope { it.copy(requireAuthentication = require) } } diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/UserPreferencesModel.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/UserPreferencesModel.kt index 54cfcb4..6ec9a8f 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/UserPreferencesModel.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/component/UserPreferencesModel.kt @@ -39,7 +39,9 @@ enum class OpenOtpAppTheme : Named { } @Serializable -enum class SortOtpDataBy(val selector: ((OtpData) -> String?)?) : Named { +enum class SortOtpDataBy( + val selector: ((OtpData) -> String?)?, +) : Named { Dont(selector = null), Issuer(selector = { data -> data.issuer?.takeIf { it.isNotBlank() } }), AccountName(selector = { data -> data.accountName?.takeIf { it.isNotBlank() } }); @@ -50,6 +52,13 @@ enum class SortOtpDataBy(val selector: ((OtpData) -> String?)?) : Named { Issuer -> stringResource(OpenOtpResources.strings.issuer_sort_name) AccountName -> stringResource(OpenOtpResources.strings.account_name_sort_name) } + + val OpenOtpAppComponentContext.defaultGroupName: String + get() = when (this@SortOtpDataBy) { + Dont -> stringResource(OpenOtpResources.strings.default_group_name_dont_sort_name) + Issuer -> stringResource(OpenOtpResources.strings.default_group_name_issuer_sort_name) + AccountName -> stringResource(OpenOtpResources.strings.default_group_name_account_name_sort_name) + } } @Serializable @@ -60,6 +69,7 @@ data class UserPreferencesModel( val sortOtpDataReversed: Boolean = false, val confirmOtpDataDelete: Boolean = true, val requireAuthentication: Boolean = false, + val showSortedGroupsHeaders: Boolean = true, ) private val LightColors: ColorScheme = lightColorScheme( diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/otp/TotpData.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/otp/TotpData.kt index a108387..feb9358 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/otp/TotpData.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/otp/TotpData.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.benasher44.uuid.uuid4 import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import ml.dev.kotlin.openotp.ui.component.DragDropListData @Immutable @Serializable @@ -103,4 +104,6 @@ data class TotpData( } } -typealias UserOtpCodeData = List +typealias StoredOtpCodeData = List + +typealias PresentedOtpCodeData = DragDropListData diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/DragDropList.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/DragDropList.kt index 8287eba..6dacc94 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/DragDropList.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/DragDropList.kt @@ -1,14 +1,22 @@ package ml.dev.kotlin.openotp.ui.component import androidx.compose.animation.core.* +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -16,13 +24,47 @@ import kotlinx.coroutines.launch import ml.dev.kotlin.openotp.util.runIfNonNull import kotlin.math.roundToInt +interface DragDropListData { + data class Listed(override val items: List) : DragDropListData { + override val isEmpty: Boolean = items.isEmpty() + + override fun filter(predicate: (U) -> Boolean): DragDropListData = + Listed(items.filter(predicate)) + } + + data class Grouped(val groups: List>) : DragDropListData { + data class Group(val groupName: String, val items: List) + + override val isEmpty: Boolean = groups.all { it.items.isEmpty() } + + override fun filter(predicate: (U) -> Boolean): DragDropListData = + Grouped(groups.map { Group(it.groupName, it.items.filter(predicate)) }) + + override val items: List by lazy { groups.flatMap { it.items } } + } + + val isEmpty: Boolean + + val items: List + + fun filter(predicate: (T) -> Boolean): DragDropListData + + companion object { + private val EMPTY: DragDropListData = Listed(emptyList()) + + fun emptyDragDropListData(): DragDropListData = EMPTY + } +} + @Composable internal fun DragDropList( - items: List, + items: DragDropListData, key: (T) -> Any, modifier: Modifier, enabled: Boolean, + showHeaders: Boolean, dragDropState: DragDropState, + headerColor: Color = MaterialTheme.colorScheme.background, itemContent: @Composable LazyItemScope.(item: T, modifier: Modifier) -> Unit, ) { var overscrollJob by remember { mutableStateOf(null) } @@ -30,8 +72,7 @@ internal fun DragDropList( LazyColumn( modifier = when { - !enabled -> modifier - else -> modifier + enabled && items is DragDropListData.Listed -> modifier .pointerInput(dragDropState) { detectDragGesturesAfterLongPress( onDrag = { change, offset -> @@ -62,18 +103,52 @@ internal fun DragDropList( } ) } + + else -> modifier }, state = dragDropState.listState, ) { - itemsIndexed( - items = items, - key = { _, item -> key(item) } - ) { index, item -> - DraggableItem( - dragDropState = dragDropState, - index = index, - ) { modifier -> - itemContent(item, modifier) + when { + showHeaders && items is DragDropListData.Grouped -> items.groups.forEach { group -> + if (group.items.isNotEmpty()) stickyHeader(group.groupName) { + Text( + text = group.groupName, + style = MaterialTheme.typography.labelLarge, + color = contentColorFor(headerColor), + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .background(headerColor) + .padding( + bottom = 12.dp, + start = 12.dp, + end = 12.dp, + ) + ) + } + itemsIndexed( + items = group.items, + key = { _, item -> key(item) } + ) { index, item -> + DraggableItem( + dragDropState = dragDropState, + index = index, + ) { modifier -> + itemContent(item, modifier) + } + } + } + + else -> itemsIndexed( + items = items.items, + key = { _, item -> key(item) } + ) { index, item -> + DraggableItem( + dragDropState = dragDropState, + index = index, + ) { modifier -> + itemContent(item, modifier) + } } } } diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/FilteredOtpCodeItems.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/FilteredOtpCodeItems.kt index 0d44039..10a9b69 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/FilteredOtpCodeItems.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/FilteredOtpCodeItems.kt @@ -21,12 +21,13 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.zIndex import dev.icerock.moko.resources.compose.stringResource import ml.dev.kotlin.openotp.otp.OtpData -import ml.dev.kotlin.openotp.otp.UserOtpCodeData +import ml.dev.kotlin.openotp.otp.PresentedOtpCodeData import ml.dev.kotlin.openotp.shared.OpenOtpResources +import ml.dev.kotlin.openotp.ui.component.DragDropListData.Companion.emptyDragDropListData @Composable internal fun FilteredOtpCodeItems( - codeData: UserOtpCodeData, + codeData: PresentedOtpCodeData, timestamp: Long, confirmCodeDismiss: Boolean, isSearchActive: Boolean, @@ -45,8 +46,8 @@ internal fun FilteredOtpCodeItems( contentAlignment = Alignment.TopCenter, ) { var searchQuery by rememberSaveable { mutableStateOf("") } - val filteredCodeData = remember(searchQuery, codeData) { - if (searchQuery.isEmpty()) emptyList() else codeData.filter { + val filteredCodeData = remember(searchQuery, codeData) filtered@{ + if (searchQuery.isEmpty()) emptyDragDropListData() else codeData.filter { it.accountName?.contains(searchQuery, ignoreCase = true) == true || it.issuer?.contains(searchQuery, ignoreCase = true) == true } @@ -109,6 +110,7 @@ internal fun FilteredOtpCodeItems( timestamp = timestamp, confirmCodeDismiss = confirmCodeDismiss, isDragAndDropEnabled = false, + showSortedGroupsHeaders = false, onOtpCodeDataDismiss = onOtpCodeDataDismiss, onRestartCode = onRestartCode, copyOtpCode = copyOtpCode, diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/OtpCodeItems.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/OtpCodeItems.kt index 196829c..0ef7862 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/OtpCodeItems.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/component/OtpCodeItems.kt @@ -35,8 +35,8 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.isActive import ml.dev.kotlin.openotp.otp.HotpData import ml.dev.kotlin.openotp.otp.OtpData +import ml.dev.kotlin.openotp.otp.PresentedOtpCodeData import ml.dev.kotlin.openotp.otp.TotpData -import ml.dev.kotlin.openotp.otp.UserOtpCodeData import ml.dev.kotlin.openotp.shared.OpenOtpResources import ml.dev.kotlin.openotp.ui.issuerIcon import ml.dev.kotlin.openotp.util.OnceLaunchedEffect @@ -46,10 +46,11 @@ import kotlin.math.roundToInt @Composable internal fun OtpCodeItems( - codeData: UserOtpCodeData, + codeData: PresentedOtpCodeData, timestamp: Long, confirmCodeDismiss: Boolean, isDragAndDropEnabled: Boolean, + showSortedGroupsHeaders: Boolean, onOtpCodeDataDismiss: (OtpData) -> Boolean, onRestartCode: (OtpData) -> Unit, dragDropState: DragDropState, @@ -63,6 +64,7 @@ internal fun OtpCodeItems( key = { it.uuid }, dragDropState = dragDropState, enabled = isDragAndDropEnabled, + showHeaders = showSortedGroupsHeaders, modifier = Modifier .fillMaxSize() .padding(top = 12.dp), diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/screen/MainScreen.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/screen/MainScreen.kt index 329a22b..b540f72 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/screen/MainScreen.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/screen/MainScreen.kt @@ -14,7 +14,7 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState import dev.icerock.moko.resources.compose.stringResource import ml.dev.kotlin.openotp.component.MainComponent import ml.dev.kotlin.openotp.otp.OtpData -import ml.dev.kotlin.openotp.otp.UserOtpCodeData +import ml.dev.kotlin.openotp.otp.PresentedOtpCodeData import ml.dev.kotlin.openotp.qr.CameraPermission.Denied import ml.dev.kotlin.openotp.qr.CameraPermission.Granted import ml.dev.kotlin.openotp.qr.rememberCameraPermissionState @@ -55,11 +55,13 @@ internal fun MainScreen(mainComponent: MainComponent) { val listState = rememberLazyListState() val dragDropState = rememberDragDropState(listState, mainComponent::onOtpCodeDataReordered) val isDragAndDropEnabled by mainComponent.isDragAndDropEnabled.subscribeAsState() + val showSortedGroupsHeaders by mainComponent.showSortedGroupsHeaders.subscribeAsState() AllOtpCodeItems( codeData = codeData, timestamp = timestamp, confirmCodeDismiss = confirmOtpDataDelete, isDragAndDropEnabled = isDragAndDropEnabled, + showSortedGroupsHeaders = showSortedGroupsHeaders, onOtpCodeDataDismiss = mainComponent::onOtpCodeDataRemove, onRestartCode = mainComponent::onOtpCodeDataRestart, copyOtpCode = mainComponent::copyOtpCode, @@ -84,10 +86,11 @@ internal fun MainScreen(mainComponent: MainComponent) { @Composable private fun AllOtpCodeItems( - codeData: UserOtpCodeData, + codeData: PresentedOtpCodeData, timestamp: Long, confirmCodeDismiss: Boolean, isDragAndDropEnabled: Boolean, + showSortedGroupsHeaders: Boolean, onOtpCodeDataDismiss: (OtpData) -> Boolean, onRestartCode: (OtpData) -> Unit, dragDropState: DragDropState, @@ -105,12 +108,13 @@ private fun AllOtpCodeItems( .weight(weight = 1f, fill = true), contentAlignment = Alignment.Center, ) { - if (codeData.isNotEmpty()) { + if (!codeData.isEmpty) { OtpCodeItems( codeData, timestamp, confirmCodeDismiss, isDragAndDropEnabled, + showSortedGroupsHeaders, onOtpCodeDataDismiss, onRestartCode, dragDropState, diff --git a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/screen/SettingsScreen.kt b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/screen/SettingsScreen.kt index 020ae81..70a724f 100644 --- a/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/screen/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/screen/SettingsScreen.kt @@ -1,6 +1,7 @@ package ml.dev.kotlin.openotp.ui.screen import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -11,8 +12,7 @@ import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.LightMode import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -121,27 +121,47 @@ private fun CodesManagementSettingsGroup(component: SettingsComponent) { anyItems = SortOtpDataBy.entries ) val canReorderDataManually by component.canReorderDataManually.subscribeAsState() - AnimatedVisibility(visible = canReorderDataManually) { + val reorderManuallyVisibleState = + remember { MutableTransitionState(false) }.apply { targetState = canReorderDataManually } + AnimatedVisibility( + visibleState = reorderManuallyVisibleState, + ) { Text( text = stringResource(OpenOtpResources.strings.can_manually_reorder), style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.alpha(0.75f) + modifier = Modifier.alpha(0.75f).padding(bottom = 12.dp) ) } + var visibleDetailedOptions by remember { mutableStateOf(!canReorderDataManually) } + visibleDetailedOptions = when { + reorderManuallyVisibleState.run { targetState && currentState } -> false + reorderManuallyVisibleState.run { !targetState && !currentState } -> true + else -> visibleDetailedOptions + } + AnimatedVisibility(visible = visibleDetailedOptions) { + Column { + val showSortedGroupsHeaders by component.showSortedGroupsHeaders.subscribeAsState() + NamedSwitch( + name = stringResource(OpenOtpResources.strings.show_headers), + checked = showSortedGroupsHeaders, + onCheckedChange = component::onShowSortedGroupsHeadersChange, + ) - val sortOtpDataNullsFirst by component.sortOtpDataNullsFirst.subscribeAsState() - NamedSwitch( - name = stringResource(OpenOtpResources.strings.nulls_first), - checked = sortOtpDataNullsFirst, - onCheckedChange = component::onSortNullsFirstChange, - ) + val sortOtpDataNullsFirst by component.sortOtpDataNullsFirst.subscribeAsState() + NamedSwitch( + name = stringResource(OpenOtpResources.strings.nulls_first), + checked = sortOtpDataNullsFirst, + onCheckedChange = component::onSortNullsFirstChange, + ) - val sortOtpDataReversed by component.sortOtpDataReversed.subscribeAsState() - NamedSwitch( - name = stringResource(OpenOtpResources.strings.reversed_sort), - checked = sortOtpDataReversed, - onCheckedChange = component::onSortReversedChange, - ) + val sortOtpDataReversed by component.sortOtpDataReversed.subscribeAsState() + NamedSwitch( + name = stringResource(OpenOtpResources.strings.reversed_sort), + checked = sortOtpDataReversed, + onCheckedChange = component::onSortReversedChange, + ) + } + } } } diff --git a/shared/src/commonMain/resources/MR/base/strings.xml b/shared/src/commonMain/resources/MR/base/strings.xml index 51ad78c..09154f8 100644 --- a/shared/src/commonMain/resources/MR/base/strings.xml +++ b/shared/src/commonMain/resources/MR/base/strings.xml @@ -16,12 +16,16 @@ Sort codes by Reverse sort order Empty values first + Show group header Light Dark System Issuer Account name Disabled + Empty issuer + Empty account name + You can manually reorder items with drag & drop gesture. Require authentication before start Authenticate diff --git a/shared/src/commonMain/resources/MR/pl/strings.xml b/shared/src/commonMain/resources/MR/pl/strings.xml index f0467cf..6d7a800 100644 --- a/shared/src/commonMain/resources/MR/pl/strings.xml +++ b/shared/src/commonMain/resources/MR/pl/strings.xml @@ -16,12 +16,16 @@ Sortuj kody po Odwróć kolejność sortowania Puste wartości na początku + Pokazuj nagłówki grup Jasny Ciemny Systemowy Wystawcy Nazwie konta Wyłączone + Brak wystawcy + Brak nazwy konta + Możliwość ręcznej zmiany kolejności kodów dzięki gestom przytrzymania i przesuwania.