From 85a91ddc5595882860732f7a56383ea1971753e8 Mon Sep 17 00:00:00 2001 From: Roman Makeev <57789105+makeevrserg@users.noreply.github.com> Date: Fri, 6 Sep 2024 20:53:12 +0300 Subject: [PATCH] Remotecontrols/better configuring (#938) **Background** Remote control configure screen is too slow and not user-friendly by it's ux. This pr will help user to understand what's going on on screen. **Changes** - When files is moving user will see files are moving - When syncing user will see animated syncing progress **Test plan** - Try add some remote and you will see new screen functionality --- CHANGELOG.md | 1 + .../composable/CreateControlComposable.kt | 132 ++++++++++++++---- .../CreateControlDecomposeComponentImpl.kt | 7 +- .../viewmodel/SaveRemoteControlViewModel.kt | 28 ++-- .../impl/src/main/res/values/strings.xml | 2 + 5 files changed, 134 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49dd6e2415..35aca8504f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Attention: don't forget to add the flag for F-Droid before release - [Feature] More UI elements for remote-controls - [Feature] Add How to Use dialog into remote-controls - [Feature] Skip infrared signals on setup screen +- [Feature] Better user-ux when configuring remote control - [Refactor] Load RemoteControls from flipper, emulating animation - [Refactor] Update to Kotlin 2.0 - [Refactor] Replace Ktorfit with Ktor requests in remote-controls diff --git a/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt index 2650fa0370..b29cb816ac 100644 --- a/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt +++ b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt @@ -1,5 +1,10 @@ package com.flipperdevices.remotecontrols.impl.createcontrol.composable +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -9,49 +14,126 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable +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.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ktx.jre.roundPercentToString import com.flipperdevices.core.ui.theme.FlipperThemeInternal import com.flipperdevices.core.ui.theme.LocalPallet import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.core.ui.theme.LocalTypography import com.flipperdevices.remotecontrols.grid.createcontrol.impl.R +import com.flipperdevices.remotecontrols.impl.createcontrol.viewmodel.SaveRemoteControlViewModel.State @Composable -internal fun CreateControlComposable() { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.configuring_title), - color = LocalPalletV2.current.text.body.primary, - style = LocalTypography.current.titleB18 - ) - Spacer(Modifier.height(24.dp)) - CircularProgressIndicator( - modifier = Modifier.size(48.dp), - color = LocalPallet.current.accentSecond - ) - Spacer(Modifier.height(8.dp)) - Text( - text = stringResource(R.string.configuring_desc), - color = LocalPalletV2.current.text.body.secondary, - style = LocalTypography.current.subtitleM12, - textAlign = TextAlign.Center - ) +internal fun CreateControlComposable(state: State) { + AnimatedContent( + targetState = state, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + contentKey = { + when (it) { + State.CouldNotModifyFiles -> 0 + is State.Finished -> 1 + State.InProgress.ModifyingFiles -> 2 + is State.InProgress.Synchronizing -> 3 + State.KeyNotFound -> 4 + State.Pending -> 5 + } + }, + content = { animatedState -> + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TitleComposable(animatedState) + Spacer(Modifier.height(24.dp)) + ProgressIndicatorComposable((animatedState as? State.InProgress.Synchronizing)?.progress) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.configuring_desc), + color = LocalPalletV2.current.text.body.secondary, + style = LocalTypography.current.subtitleM12, + textAlign = TextAlign.Center + ) + } + } + ) +} + +@Composable +private fun TitleComposable( + state: State, + modifier: Modifier = Modifier +) { + Text( + text = when (state) { + State.InProgress.ModifyingFiles -> stringResource(R.string.configuring_files_title) + is State.InProgress.Synchronizing -> { + val progressAnimated by animateFloatAsState(state.progress) + LocalContext.current.getString( + R.string.archive_sync_percent, + progressAnimated.roundPercentToString() + ) + } + + else -> stringResource(R.string.configuring_title) + }, + color = LocalPalletV2.current.text.body.primary, + style = LocalTypography.current.titleB18, + modifier = modifier + ) +} + +@Composable +private fun ProgressIndicatorComposable( + progress: Float?, + modifier: Modifier = Modifier +) { + when { + progress != null -> { + val progressAnimated by animateFloatAsState(progress) + CircularProgressIndicator( + modifier = modifier.size(48.dp), + color = LocalPallet.current.accentSecond, + progress = progressAnimated + ) + } + + else -> { + CircularProgressIndicator( + modifier = modifier.size(48.dp), + color = LocalPallet.current.accentSecond, + ) + } + } +} + +@Preview +@Composable +private fun CreateControlComposablePendingPreview() { + FlipperThemeInternal { + CreateControlComposable(State.Pending) + } +} + +@Preview +@Composable +private fun CreateControlComposableModifyingPreview() { + FlipperThemeInternal { + CreateControlComposable(State.InProgress.ModifyingFiles) } } @Preview @Composable -private fun CreateControlComposablePreview() { +private fun CreateControlComposableSyncingPreview() { FlipperThemeInternal { - CreateControlComposable() + CreateControlComposable(State.InProgress.Synchronizing(progress = 0.3f)) } } diff --git a/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/decompose/CreateControlDecomposeComponentImpl.kt b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/decompose/CreateControlDecomposeComponentImpl.kt index 271e9e979b..6bd19dce0f 100644 --- a/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/decompose/CreateControlDecomposeComponentImpl.kt +++ b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/decompose/CreateControlDecomposeComponentImpl.kt @@ -2,6 +2,8 @@ package com.flipperdevices.remotecontrols.impl.createcontrol.decompose import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import com.arkivanov.decompose.ComponentContext import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath import com.flipperdevices.core.di.AppGraph @@ -35,6 +37,7 @@ class CreateControlDecomposeComponentImpl @AssistedInject constructor( key = null, factory = { saveRemoteControlViewModelFactory.get() } ) + val state by saveRemoteControlViewModel.state.collectAsState() val rootNavigation = LocalRootNavigation.current LaunchedEffect(saveRemoteControlViewModel) { saveRemoteControlViewModel.state @@ -51,7 +54,7 @@ class CreateControlDecomposeComponentImpl @AssistedInject constructor( } SaveRemoteControlViewModel.State.Pending, - SaveRemoteControlViewModel.State.Updating -> Unit + is SaveRemoteControlViewModel.State.InProgress -> Unit } }.launchIn(this) saveRemoteControlViewModel.moveAndUpdate( @@ -59,6 +62,6 @@ class CreateControlDecomposeComponentImpl @AssistedInject constructor( originalKey = originalKey, ) } - CreateControlComposable() + CreateControlComposable(state) } } diff --git a/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/viewmodel/SaveRemoteControlViewModel.kt b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/viewmodel/SaveRemoteControlViewModel.kt index b0ce7020d0..d4d77e9cf1 100644 --- a/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/viewmodel/SaveRemoteControlViewModel.kt +++ b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/viewmodel/SaveRemoteControlViewModel.kt @@ -11,7 +11,6 @@ import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider import com.flipperdevices.bridge.synchronization.api.SynchronizationApi import com.flipperdevices.bridge.synchronization.api.SynchronizationState import com.flipperdevices.core.log.LogTagProvider -import com.flipperdevices.core.log.info import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel import com.flipperdevices.keyedit.api.NotSavedFlipperFile import com.flipperdevices.keyedit.api.NotSavedFlipperKey @@ -21,10 +20,13 @@ import com.flipperdevices.protobuf.storage.deleteRequest import com.flipperdevices.protobuf.storage.renameRequest import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.job import kotlinx.coroutines.launch @@ -77,18 +79,24 @@ class SaveRemoteControlViewModel @Inject constructor( } } - private suspend fun awaitSynchronization() { + private suspend fun awaitSynchronization( + onChange: (progress: Float) -> Unit + ): Unit = coroutineScope { if (!synchronizationApi.isSynchronizationRunning()) { synchronizationApi.startSynchronization(force = true) } + val progressJon = synchronizationApi.getSynchronizationState() + .filterIsInstance() + .onEach { onChange.invoke(it.progress) } + .launchIn(this) synchronizationApi.getSynchronizationState() - .onEach { info { "#moveAndUpdate $it" } } .filterIsInstance() + .onEach { onChange.invoke(it.progress) } .first() synchronizationApi.getSynchronizationState() - .onEach { info { "#moveAndUpdate $it" } } .filterIsInstance() .first() + progressJon.cancelAndJoin() } /** @@ -124,12 +132,10 @@ class SaveRemoteControlViewModel @Inject constructor( originalKey: NotSavedFlipperKey, ) { viewModelScope.launch { - _state.emit(State.Updating) + _state.emit(State.InProgress.ModifyingFiles) if (lastMoveJob != null) lastMoveJob?.join() lastMoveJob = coroutineContext.job - awaitSynchronization() - val flipperKey = simpleKeyApi.getKey(savedKeyPath) ?: run { _state.emit(State.KeyNotFound) return@launch @@ -153,7 +159,7 @@ class SaveRemoteControlViewModel @Inject constructor( }.toImmutableList() ) ) - awaitSynchronization() + awaitSynchronization(onChange = { _state.value = State.InProgress.Synchronizing(it) }) val keyPath = FlipperKeyPath( path = flipperKey.mainFile.path.toNonTempPath(), deleted = false @@ -164,7 +170,11 @@ class SaveRemoteControlViewModel @Inject constructor( sealed interface State { data object Pending : State - data object Updating : State + sealed interface InProgress : State { + data object ModifyingFiles : InProgress + data class Synchronizing(val progress: Float) : InProgress + } + data class Finished(val keyPath: FlipperKeyPath) : State data object KeyNotFound : State data object CouldNotModifyFiles : State diff --git a/components/remote-controls/grid/create-control/impl/src/main/res/values/strings.xml b/components/remote-controls/grid/create-control/impl/src/main/res/values/strings.xml index 38349202ed..5d8ee076b1 100644 --- a/components/remote-controls/grid/create-control/impl/src/main/res/values/strings.xml +++ b/components/remote-controls/grid/create-control/impl/src/main/res/values/strings.xml @@ -2,5 +2,7 @@ Configuring + Configuring Remote control is being configured. Please do not close this screen + Syncing %1$s \ No newline at end of file