diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f06974089..983380d272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,13 @@ # 1.6.7 - In progress +- [Feature] Infrared Editor process error +- [Feature] Optimization FapHub by compose metrics - [FIX] Splashscreen WearOS icon # 1.6.6 - [Feature] Bump deps -- [Feature] Optimization FapHub by compose metrics - [FIX] Fix faphub minor version supported suggest to update - [FIX] Fix faphub manifest minor api version - [FIX] Fix faphub manifest image base64 diff --git a/components/infrared/editor/build.gradle.kts b/components/infrared/editor/build.gradle.kts index 655bdf4dd2..afcbbae3ad 100644 --- a/components/infrared/editor/build.gradle.kts +++ b/components/infrared/editor/build.gradle.kts @@ -16,8 +16,11 @@ dependencies { implementation(projects.components.bridge.dao.api) implementation(projects.components.bridge.synchronization.api) + implementation(projects.components.bridge.api) implementation(projects.components.core.di) + implementation(projects.components.core.ktx) + implementation(projects.components.core.log) implementation(projects.components.core.ui.navigation) implementation(projects.components.core.ui.theme) implementation(projects.components.core.ui.tabswitch) diff --git a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/components/ComposableInfraredEditorItem.kt b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/components/ComposableInfraredEditorItem.kt index 6be0028d09..983ab6d77e 100644 --- a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/components/ComposableInfraredEditorItem.kt +++ b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/components/ComposableInfraredEditorItem.kt @@ -1,41 +1,78 @@ package com.flipperdevices.infrared.editor.compose.components import android.content.res.Configuration +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material.ContentAlpha import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.ktx.clickableRipple import com.flipperdevices.core.ui.theme.FlipperThemeInternal import com.flipperdevices.core.ui.theme.LocalPallet import com.flipperdevices.core.ui.theme.LocalTypography import com.flipperdevices.infrared.editor.R +import kotlin.math.roundToInt import com.flipperdevices.core.ui.res.R as DesignSystem +private const val BUTTON_HEIGHT = 55 +private const val COUNT_OF_SHAKE = 10 +private const val TARGET_SHAKE = 5f +private const val DELTA_SHAKE = 100_000f + @Composable internal fun ComposableInfraredEditorItem( remoteName: String, - onTap: () -> Unit, + onChangeName: (String) -> Unit, + onChangeIndexEditor: () -> Unit, onDelete: () -> Unit, + isActive: Boolean, + isError: Boolean, modifier: Modifier = Modifier, - dragModifier: Modifier = Modifier + dragModifier: Modifier = Modifier, ) { + val shake = getShakeAnimation(isError) Row( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .offset { IntOffset(shake, y = 0) }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { @@ -43,7 +80,9 @@ internal fun ComposableInfraredEditorItem( modifier = Modifier.weight(1f), dragModifier = dragModifier, remoteName = remoteName, - onTap = onTap + onChangeName = onChangeName, + isActiveEditor = isActive, + onChangeIndexEditor = onChangeIndexEditor ) Icon( modifier = Modifier @@ -57,20 +96,46 @@ internal fun ComposableInfraredEditorItem( } } +@Composable +private fun getShakeAnimation(isError: Boolean): Int { + val shake = remember { Animatable(0f) } + var trigger by remember { mutableStateOf(false) } + + LaunchedEffect(isError) { + if (isError) { trigger = true } + } + + LaunchedEffect(trigger) { + if (trigger.not()) return@LaunchedEffect + for (i in 0..COUNT_OF_SHAKE) { + when (i % 2) { + 0 -> shake.animateTo(TARGET_SHAKE, spring(stiffness = DELTA_SHAKE)) + else -> shake.animateTo(-TARGET_SHAKE, spring(stiffness = DELTA_SHAKE)) + } + } + shake.animateTo(0f) + } + + return shake.value.roundToInt() +} + @Composable private fun ComposableInfraredEditorButton( remoteName: String, - onTap: () -> Unit, + onChangeName: (String) -> Unit, + onChangeIndexEditor: () -> Unit, + isActiveEditor: Boolean, modifier: Modifier = Modifier, - dragModifier: Modifier = Modifier + dragModifier: Modifier = Modifier, ) { Row( modifier = modifier + .heightIn(min = BUTTON_HEIGHT.dp) .clip(shape = RoundedCornerShape(12.dp)) - .clickable(onClick = onTap) .background(LocalPallet.current.accent) .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center ) { Icon( modifier = dragModifier @@ -80,14 +145,83 @@ private fun ComposableInfraredEditorButton( contentDescription = null, tint = LocalPallet.current.infraredEditorDrag ) - Text( - text = remoteName, - modifier = Modifier.weight(1f), - style = LocalTypography.current.infraredEditButton, - color = LocalPallet.current.infraredEditorKeyName, - textAlign = TextAlign.Center + + if (isActiveEditor) { + ComposableInfraredEditorField(remoteName, onChangeName) + } else { + ComposableInfraredEditorText(remoteName, onChangeIndexEditor) + } + } +} + +@Composable +private fun RowScope.ComposableInfraredEditorField( + remoteName: String, + onChangeName: (String) -> Unit, +) { + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + + val customTextSelectionColors = TextSelectionColors( + handleColor = LocalPallet.current.accentSecond, + backgroundColor = LocalPallet.current.accent.copy(alpha = ContentAlpha.high) + ) + val textState by remember(remoteName) { + mutableStateOf( + TextFieldValue( + text = remoteName, + selection = TextRange(remoteName.length) + ) + ) + } + + CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) { + TextField( + value = textState, + onValueChange = { + onChangeName(it.text) + }, + singleLine = true, + modifier = Modifier + .focusRequester(focusRequester) + .weight(1f), + textStyle = LocalTypography.current.infraredEditButton.copy( + textAlign = TextAlign.Center + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + }, + ), + colors = TextFieldDefaults.textFieldColors( + textColor = LocalPallet.current.infraredEditorKeyName, + cursorColor = LocalPallet.current.infraredEditorKeyName, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + backgroundColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) ) } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +private fun RowScope.ComposableInfraredEditorText(remoteName: String, onClick: () -> Unit) { + Text( + text = remoteName, + modifier = Modifier + .clickable(onClick = onClick) + .weight(1f), + style = LocalTypography.current.infraredEditButton, + color = LocalPallet.current.infraredEditorKeyName, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } @Preview @@ -95,9 +229,12 @@ private fun ComposableInfraredEditorButton( private fun PreviewComposableInfraredEditorItem() { FlipperThemeInternal { ComposableInfraredEditorItem( - remoteName = "012345678901234567891", + remoteName = "01234567890123456789124242424", onDelete = {}, - onTap = {} + onChangeName = {}, + onChangeIndexEditor = {}, + isActive = true, + isError = false ) } } @@ -107,9 +244,27 @@ private fun PreviewComposableInfraredEditorItem() { private fun PreviewComposableInfraredEditorItemDark() { FlipperThemeInternal { ComposableInfraredEditorItem( - remoteName = "Off", + remoteName = "01234567890123456789124242424", + onDelete = {}, + onChangeName = {}, + onChangeIndexEditor = {}, + isActive = true, + isError = false + ) + } +} + +@Preview +@Composable +private fun PreviewComposableInfraredEditorItemNotActive() { + FlipperThemeInternal { + ComposableInfraredEditorItem( + remoteName = "0123456789012345678912424242433333", onDelete = {}, - onTap = {} + onChangeName = {}, + onChangeIndexEditor = {}, + isActive = false, + isError = false ) } } diff --git a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/screen/ComposableInfraredEditorScreen.kt b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/screen/ComposableInfraredEditorScreen.kt index bad97d0be6..22133b9e1d 100644 --- a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/screen/ComposableInfraredEditorScreen.kt +++ b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/screen/ComposableInfraredEditorScreen.kt @@ -1,11 +1,12 @@ package com.flipperdevices.infrared.editor.compose.screen +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource import com.flipperdevices.infrared.editor.model.InfraredEditorState -import com.flipperdevices.infrared.editor.viewmodel.InfraredViewModel +import com.flipperdevices.infrared.editor.viewmodel.InfraredEditorViewModel import com.flipperdevices.keyscreen.shared.screen.ComposableKeyScreenError import com.flipperdevices.keyscreen.shared.screen.ComposableKeyScreenLoading import tangle.viewmodel.compose.tangleViewModel @@ -13,11 +14,15 @@ import tangle.viewmodel.compose.tangleViewModel @Composable internal fun ComposableInfraredEditorScreen( onBack: () -> Unit, - viewModel: InfraredViewModel = tangleViewModel() + viewModel: InfraredEditorViewModel = tangleViewModel() ) { val keyState by viewModel.getKeyState().collectAsState() val dialogState by viewModel.getDialogState().collectAsState() + BackHandler { + viewModel.processCancel(keyState, onBack) + } + when (val localState = keyState) { InfraredEditorState.InProgress -> ComposableKeyScreenLoading() is InfraredEditorState.Error -> ComposableKeyScreenError( @@ -29,11 +34,35 @@ internal fun ComposableInfraredEditorScreen( dialogState = dialogState, onDoNotSave = onBack, onDismissDialog = viewModel::onDismissDialog, - onCancel = { viewModel.processCancel(onBack) }, - onSave = { viewModel.processSave(onBack) }, - onTapRemote = {}, - onDelete = viewModel::processDeleteRemote, - onEditOrder = viewModel::processEditOrder + onCancel = { + viewModel.processCancel(keyState, onBack) + }, + onSave = { + viewModel.processSave(currentState = localState, onBack) + }, + onChangeName = { index, value -> + viewModel.editRemoteName( + currentState = localState, + index = index, + source = value + ) + }, + onDelete = { + viewModel.processDeleteRemote( + currentState = localState, + index = it + ) + }, + onEditOrder = { from, to -> + viewModel.processEditOrder( + currentState = localState, + from = from, + to = to + ) + }, + onChangeIndexEditor = { + viewModel.processChangeIndexEditor(localState, it) + } ) } } diff --git a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/screen/ComposableInfraredEditorScreenReady.kt b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/screen/ComposableInfraredEditorScreenReady.kt index 42df5753c8..62bcd9f2c5 100644 --- a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/screen/ComposableInfraredEditorScreenReady.kt +++ b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/compose/screen/ComposableInfraredEditorScreenReady.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -29,9 +30,10 @@ internal fun ComposableInfraredEditorScreenReady( onDismissDialog: () -> Unit, onCancel: () -> Unit, onSave: () -> Unit, - onTapRemote: (Int) -> Unit, + onChangeName: (Int, String) -> Unit, onDelete: (Int) -> Unit, onEditOrder: (Int, Int) -> Unit, + onChangeIndexEditor: (Int) -> Unit, ) { ComposableInfraredEditorDialog( isShow = dialogState, @@ -57,9 +59,14 @@ internal fun ComposableInfraredEditorScreenReady( onEditOrder(from.index, to.index) }) + LaunchedEffect(keyState) { + val firstErrorRemote = keyState.errorRemotes.firstOrNull() ?: return@LaunchedEffect + state.listState.scrollToItem(firstErrorRemote) + } + LazyColumn( state = state.listState, - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier .reorderable(state) @@ -73,9 +80,12 @@ internal fun ComposableInfraredEditorScreenReady( ) { ComposableInfraredEditorItem( remoteName = remote.name, - onTap = { onTapRemote(index) }, + onChangeName = { onChangeName(index, it) }, onDelete = { onDelete(index) }, - dragModifier = Modifier.detectReorderAfterLongPress(state) + dragModifier = Modifier.detectReorderAfterLongPress(state), + onChangeIndexEditor = { onChangeIndexEditor(index) }, + isActive = keyState.activeRemote == index, + isError = index in keyState.errorRemotes, ) } } diff --git a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/model/InfraredEditorState.kt b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/model/InfraredEditorState.kt index 07f8d8a40b..9e985ac461 100644 --- a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/model/InfraredEditorState.kt +++ b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/model/InfraredEditorState.kt @@ -2,12 +2,15 @@ package com.flipperdevices.infrared.editor.model import androidx.annotation.StringRes import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf sealed interface InfraredEditorState { data object InProgress : InfraredEditorState data class Error(@StringRes val reason: Int) : InfraredEditorState data class Ready( val remotes: ImmutableList, - val keyName: String + val keyName: String, + val activeRemote: Int? = null, + val errorRemotes: ImmutableList = persistentListOf(), ) : InfraredEditorState } diff --git a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/model/InfraredRemote.kt b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/model/InfraredRemote.kt index 366c829a0c..2615813e94 100644 --- a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/model/InfraredRemote.kt +++ b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/model/InfraredRemote.kt @@ -19,4 +19,11 @@ sealed class InfraredRemote( val dutyCycle: String, val data: String, ) : InfraredRemote(nameInternal, typeInternal) + + fun copy(name: String): InfraredRemote { + return when (this) { + is Parsed -> copy(nameInternal = name) + is Raw -> copy(nameInternal = name) + } + } } diff --git a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/viewmodel/InfraredEditorViewModel.kt b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/viewmodel/InfraredEditorViewModel.kt new file mode 100644 index 0000000000..a5f8a54b5e --- /dev/null +++ b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/viewmodel/InfraredEditorViewModel.kt @@ -0,0 +1,230 @@ +package com.flipperdevices.infrared.editor.viewmodel + +import android.content.Context +import android.os.Vibrator +import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.flipperdevices.bridge.api.utils.FlipperSymbolFilter +import com.flipperdevices.bridge.dao.api.delegates.key.SimpleKeyApi +import com.flipperdevices.bridge.dao.api.delegates.key.UpdateKeyApi +import com.flipperdevices.bridge.dao.api.model.FlipperFileFormat +import com.flipperdevices.bridge.dao.api.model.FlipperKey +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.bridge.synchronization.api.SynchronizationApi +import com.flipperdevices.core.ktx.android.vibrateCompat +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.info +import com.flipperdevices.infrared.editor.R +import com.flipperdevices.infrared.editor.api.EXTRA_KEY_PATH +import com.flipperdevices.infrared.editor.model.InfraredEditorState +import com.flipperdevices.infrared.editor.model.InfraredRemote +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import tangle.inject.TangleParam +import tangle.viewmodel.VMInject +import java.nio.charset.Charset + +private const val MAX_SIZE_REMOTE_LENGTH = 21 +private const val VIBRATOR_TIME_MS = 500L + +@Suppress("TooManyFunctions") +class InfraredEditorViewModel @VMInject constructor( + @TangleParam(EXTRA_KEY_PATH) + private val keyPath: FlipperKeyPath, + private val simpleKeyApi: SimpleKeyApi, + private val updateKeyApi: UpdateKeyApi, + private val synchronizationApi: SynchronizationApi, + context: Context +) : ViewModel(), LogTagProvider { + override val TAG: String = "InfraredEditorViewModel" + + private var vibrator = ContextCompat.getSystemService(context, Vibrator::class.java) + + private val flipperKeyFlow = MutableStateFlow(null) + private val flipperParsedKeyFlow = MutableStateFlow?>(null) + + private val keyStateFlow = MutableStateFlow(InfraredEditorState.InProgress) + fun getKeyState() = keyStateFlow.asStateFlow() + + private val dialogStateFlow = MutableStateFlow(false) + fun getDialogState() = dialogStateFlow.asStateFlow() + + fun onDismissDialog() = viewModelScope.launch { + dialogStateFlow.emit(false) + } + + init { invalidate() } + + private fun invalidate() { + viewModelScope.launch(Dispatchers.Default) { + val flipperKey = simpleKeyApi.getKey(keyPath) + if (flipperKey == null) { + keyStateFlow.emit(InfraredEditorState.Error(R.string.infrared_editor_not_found_key)) + return@launch + } + + val fileContent = flipperKey.keyContent.openStream().use { + it.readBytes().toString(Charset.defaultCharset()) + } + val fff = FlipperFileFormat.fromFileContent(fileContent) + val infraredParsed = InfraredKeyParser + .mapParsedKeyToInfraredRemotes(fff) + .toPersistentList() + + flipperKeyFlow.emit(flipperKey) + flipperParsedKeyFlow.emit(infraredParsed) + + keyStateFlow.emit( + InfraredEditorState.Ready( + keyName = flipperKey.path.nameWithoutExtension, + remotes = infraredParsed + ) + ) + } + } + + fun processSave( + currentState: InfraredEditorState.Ready, + onExitScreen: () -> Unit + ) = viewModelScope.launch(Dispatchers.Default) { + if (isDirtyKey(currentState).not()) { + withContext(Dispatchers.Main) { + onExitScreen() + } + return@launch + } + val errorRemotes = getErrorRemotes(currentState) + info { "Errors remote: $errorRemotes count ${errorRemotes.size}" } + + if (errorRemotes.isNotEmpty()) { + vibrator?.vibrateCompat(VIBRATOR_TIME_MS) + keyStateFlow.emit( + InfraredEditorState.Ready( + remotes = currentState.remotes, + keyName = currentState.keyName, + errorRemotes = errorRemotes + ) + ) + return@launch + } + val flipperKey = flipperKeyFlow.first() ?: return@launch + val newFlipperKey = InfraredStateParser.mapStateToFlipperKey(flipperKey, currentState) + + updateKeyApi.updateKey(flipperKey, newFlipperKey) + synchronizationApi.startSynchronization(force = true) + + withContext(Dispatchers.Main) { + onExitScreen() + } + } + + private fun getErrorRemotes(state: InfraredEditorState.Ready): ImmutableList { + val remotes = state.remotes + val errorRemotes = mutableListOf() + + remotes.forEachIndexed { index, remote -> + val countRepeat = remotes.count { it.name == remote.name } + if (remote.name.isEmpty() || countRepeat > 1) { + errorRemotes.add(index) + } + } + + return errorRemotes.toPersistentList() + } + + fun processCancel( + currentState: InfraredEditorState, + onExitScreen: () -> Unit + ) = viewModelScope.launch(Dispatchers.Default) { + if (isDirtyKey(currentState)) { + dialogStateFlow.emit(true) + } else { + withContext(Dispatchers.Main) { + onExitScreen() + } + } + } + + fun processDeleteRemote( + currentState: InfraredEditorState.Ready, + index: Int + ) = viewModelScope.launch(Dispatchers.Default) { + val remotes = currentState.remotes.toMutableList() + remotes.removeAt(index) + + keyStateFlow.emit( + InfraredEditorState.Ready( + remotes = remotes.toPersistentList(), + keyName = currentState.keyName + ) + ) + } + + private suspend fun isDirtyKey(currentState: InfraredEditorState): Boolean { + if (currentState !is InfraredEditorState.Ready) return false + val currentRemotes = currentState.remotes + val initRemotes = flipperParsedKeyFlow.first() ?: return false + + return currentRemotes != initRemotes + } + + fun processEditOrder( + currentState: InfraredEditorState.Ready, + from: Int, + to: Int + ) = viewModelScope.launch(Dispatchers.Default) { + val remotes = currentState.remotes.toMutableList() + remotes.add(to, remotes.removeAt(from)) + + keyStateFlow.emit( + InfraredEditorState.Ready( + remotes = remotes.toPersistentList(), + keyName = currentState.keyName + ) + ) + } + + fun editRemoteName( + currentState: InfraredEditorState.Ready, + index: Int, + source: String + ) { + var value = FlipperSymbolFilter.filterUnacceptableSymbol(source) + if (value.length > MAX_SIZE_REMOTE_LENGTH) { + value = value.substring(0, MAX_SIZE_REMOTE_LENGTH) + vibrator?.vibrateCompat(VIBRATOR_TIME_MS) + } + viewModelScope.launch(Dispatchers.Default) { + val remotes = currentState.remotes.toMutableList() + remotes[index] = remotes[index].copy(name = value) + + keyStateFlow.emit( + InfraredEditorState.Ready( + remotes = remotes.toPersistentList(), + keyName = currentState.keyName, + activeRemote = currentState.activeRemote + ) + ) + } + } + + fun processChangeIndexEditor( + currentState: InfraredEditorState.Ready, + index: Int + ) = viewModelScope.launch(Dispatchers.Default) { + keyStateFlow.emit( + InfraredEditorState.Ready( + remotes = currentState.remotes, + keyName = currentState.keyName, + activeRemote = index + ) + ) + } +} diff --git a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/viewmodel/InfraredViewModel.kt b/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/viewmodel/InfraredViewModel.kt deleted file mode 100644 index 648f0e8205..0000000000 --- a/components/infrared/editor/src/main/kotlin/com/flipperdevices/infrared/editor/viewmodel/InfraredViewModel.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.flipperdevices.infrared.editor.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.flipperdevices.bridge.dao.api.delegates.key.SimpleKeyApi -import com.flipperdevices.bridge.dao.api.delegates.key.UpdateKeyApi -import com.flipperdevices.bridge.dao.api.model.FlipperFileFormat -import com.flipperdevices.bridge.dao.api.model.FlipperKey -import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath -import com.flipperdevices.bridge.synchronization.api.SynchronizationApi -import com.flipperdevices.infrared.editor.R -import com.flipperdevices.infrared.editor.api.EXTRA_KEY_PATH -import com.flipperdevices.infrared.editor.model.InfraredEditorState -import com.flipperdevices.infrared.editor.model.InfraredRemote -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import tangle.inject.TangleParam -import tangle.viewmodel.VMInject -import java.nio.charset.Charset - -class InfraredViewModel @VMInject constructor( - @TangleParam(EXTRA_KEY_PATH) - private val keyPath: FlipperKeyPath, - private val simpleKeyApi: SimpleKeyApi, - private val updateKeyApi: UpdateKeyApi, - private val synchronizationApi: SynchronizationApi -) : ViewModel() { - private val keyStateFlow = MutableStateFlow(InfraredEditorState.InProgress) - private val flipperKeyFlow = MutableStateFlow(null) - private val flipperParsedKeyFlow = MutableStateFlow?>(null) - - fun getKeyState() = keyStateFlow.asStateFlow() - - private val dialogStateFlow = MutableStateFlow(false) - fun getDialogState() = dialogStateFlow.asStateFlow() - - fun onDismissDialog() = viewModelScope.launch { - dialogStateFlow.emit(false) - } - - init { invalidate() } - - private fun invalidate() { - viewModelScope.launch(Dispatchers.Default) { - val flipperKey = simpleKeyApi.getKey(keyPath) - if (flipperKey == null) { - keyStateFlow.emit(InfraredEditorState.Error(R.string.infrared_editor_not_found_key)) - return@launch - } - - val fileContent = flipperKey.keyContent.openStream().use { - it.readBytes().toString(Charset.defaultCharset()) - } - val fff = FlipperFileFormat.fromFileContent(fileContent) - val infraredParsed = InfraredKeyParser - .mapParsedKeyToInfraredRemotes(fff) - .toPersistentList() - - flipperKeyFlow.emit(flipperKey) - flipperParsedKeyFlow.emit(infraredParsed) - - keyStateFlow.emit( - InfraredEditorState.Ready( - keyName = flipperKey.path.nameWithoutExtension, - remotes = infraredParsed - ) - ) - } - } - - fun processSave(onEndAction: () -> Unit) { - viewModelScope.launch(Dispatchers.Default) { - if (isDirtyKey().not()) { - onEndAction() - return@launch - } - val remotes = keyStateFlow.first() as? InfraredEditorState.Ready ?: return@launch - val flipperKey = flipperKeyFlow.first() ?: return@launch - val newFlipperKey = InfraredStateParser.mapStateToFlipperKey(flipperKey, remotes) - - updateKeyApi.updateKey(flipperKey, newFlipperKey) - synchronizationApi.startSynchronization(force = true) - - withContext(Dispatchers.Main) { - onEndAction() - } - } - } - - fun processCancel(onEndAction: () -> Unit) { - viewModelScope.launch { - if (isDirtyKey()) { - dialogStateFlow.emit(true) - } else { - withContext(Dispatchers.Main) { - onEndAction() - } - } - } - } - - fun processDeleteRemote(index: Int) { - viewModelScope.launch(Dispatchers.Default) { - val keyState = keyStateFlow.first() as? InfraredEditorState.Ready ?: return@launch - val remotes = keyState.remotes.toMutableList() - remotes.removeAt(index) - - keyStateFlow.emit( - InfraredEditorState.Ready( - remotes = remotes.toPersistentList(), - keyName = keyState.keyName - ) - ) - } - } - - private suspend fun isDirtyKey(): Boolean { - val keyState = keyStateFlow.first() - if (keyState !is InfraredEditorState.Ready) return false - val currentRemotes = keyState.remotes - val initRemotes = flipperParsedKeyFlow.first() ?: return false - - return currentRemotes != initRemotes - } - - fun processEditOrder(from: Int, to: Int) { - viewModelScope.launch(Dispatchers.Default) { - val keyState = keyStateFlow.first() - if (keyState !is InfraredEditorState.Ready) return@launch - - val remotes = keyState.remotes.toMutableList() - remotes.add(to, remotes.removeAt(from)) - - keyStateFlow.emit( - InfraredEditorState.Ready( - remotes = remotes.toPersistentList(), - keyName = keyState.keyName - ) - ) - } - } -}