Skip to content

Commit

Permalink
Remotecontrols/better configuring (#938)
Browse files Browse the repository at this point in the history
**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
  • Loading branch information
makeevrserg authored Sep 6, 2024
1 parent 838796f commit 85a91dd
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -51,14 +54,14 @@ class CreateControlDecomposeComponentImpl @AssistedInject constructor(
}

SaveRemoteControlViewModel.State.Pending,
SaveRemoteControlViewModel.State.Updating -> Unit
is SaveRemoteControlViewModel.State.InProgress -> Unit
}
}.launchIn(this)
saveRemoteControlViewModel.moveAndUpdate(
savedKeyPath = savedKey,
originalKey = originalKey,
)
}
CreateControlComposable()
CreateControlComposable(state)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<SynchronizationState.InProgress>()
.onEach { onChange.invoke(it.progress) }
.launchIn(this)
synchronizationApi.getSynchronizationState()
.onEach { info { "#moveAndUpdate $it" } }
.filterIsInstance<SynchronizationState.InProgress>()
.onEach { onChange.invoke(it.progress) }
.first()
synchronizationApi.getSynchronizationState()
.onEach { info { "#moveAndUpdate $it" } }
.filterIsInstance<SynchronizationState.Finished>()
.first()
progressJon.cancelAndJoin()
}

/**
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
<resources>
<!-- Settings-->
<string name="configuring_title">Configuring</string>
<string name="configuring_files_title">Configuring</string>
<string name="configuring_desc">Remote control is being configured. Please do not close this screen</string>
<string name="archive_sync_percent">Syncing %1$s</string>
</resources>

0 comments on commit 85a91dd

Please sign in to comment.