diff --git a/.github/workflows/call-gradle-cache.yml b/.github/workflows/call-gradle-cache.yml index fd5f332cbf..f0f6a7f7eb 100644 --- a/.github/workflows/call-gradle-cache.yml +++ b/.github/workflows/call-gradle-cache.yml @@ -23,7 +23,7 @@ jobs: gradle.properties matrx_update_gradle: name: "Build ${{ matrix.target }}" - runs-on: [ self-hosted, AndroidShell ] + runs-on: ubuntu-latest needs: check_gradle_files_change if: needs.check_gradle_files_change.outputs.GRADLE_FILES_CHANGED == 'true' strategy: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3934609ea8..f5b966b3cd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -43,7 +43,7 @@ jobs: echo "github.repository=${{ github.repository }}" test: name: "Run unit tests" - runs-on: [ self-hosted, AndroidShell ] + runs-on: ubuntu-latest needs: [ validate_gradle_wrapper ] steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 @@ -60,7 +60,7 @@ jobs: arguments: testDebugUnitTest desktopTest detekt: name: "Check project by detekt and android`s lint" - runs-on: [ self-hosted, AndroidShell ] + runs-on: ubuntu-latest needs: [ validate_gradle_wrapper ] steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3582e8ea94..4c0ec98b32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Attention: don't forget to add the flag for F-Droid before release - [Feature] Infrared controls - [Feature] Remove bond on retry pair - [Feature] Add onetap widget +- [Feature] Save, edit, share 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/bridge/dao/api/src/main/java/com/flipperdevices/bridge/dao/api/model/FlipperFileType.kt b/components/bridge/dao/api/src/main/java/com/flipperdevices/bridge/dao/api/model/FlipperFileType.kt index 0e72dd6e72..7ff8e1c51f 100644 --- a/components/bridge/dao/api/src/main/java/com/flipperdevices/bridge/dao/api/model/FlipperFileType.kt +++ b/components/bridge/dao/api/src/main/java/com/flipperdevices/bridge/dao/api/model/FlipperFileType.kt @@ -1,6 +1,7 @@ package com.flipperdevices.bridge.dao.api.model const val SHADOW_FILE_EXTENSION = "shd" +const val UI_INFRARED_EXTENSION = "irui" /** * Order is important @@ -8,12 +9,14 @@ const val SHADOW_FILE_EXTENSION = "shd" enum class FlipperFileType { KEY, SHADOW_NFC, - OTHER; + OTHER, + UI_INFRARED; companion object { fun getByExtension(extension: String): FlipperFileType { return when (extension) { SHADOW_FILE_EXTENSION -> SHADOW_NFC + UI_INFRARED_EXTENSION -> UI_INFRARED else -> if (FlipperKeyType.getByExtension(extension) != null) { KEY } else { diff --git a/components/bridge/dao/api/src/main/java/com/flipperdevices/bridge/dao/api/model/FlipperKeyContent.kt b/components/bridge/dao/api/src/main/java/com/flipperdevices/bridge/dao/api/model/FlipperKeyContent.kt index efc70514d7..8278706d65 100644 --- a/components/bridge/dao/api/src/main/java/com/flipperdevices/bridge/dao/api/model/FlipperKeyContent.kt +++ b/components/bridge/dao/api/src/main/java/com/flipperdevices/bridge/dao/api/model/FlipperKeyContent.kt @@ -15,6 +15,7 @@ import java.io.InputStream * May be a stream, a file, a link, or bytes. * Do not limit your support to only one type of content. */ +@Serializable sealed class FlipperKeyContent : Parcelable { @Parcelize data class RawData(val bytes: ByteArray) : FlipperKeyContent() { diff --git a/components/bridge/dao/impl/src/main/java/com/flipperdevices/bridge/dao/impl/api/FlipperFileApiImpl.kt b/components/bridge/dao/impl/src/main/java/com/flipperdevices/bridge/dao/impl/api/FlipperFileApiImpl.kt index 296bd86e8e..2ad42d54b0 100644 --- a/components/bridge/dao/impl/src/main/java/com/flipperdevices/bridge/dao/impl/api/FlipperFileApiImpl.kt +++ b/components/bridge/dao/impl/src/main/java/com/flipperdevices/bridge/dao/impl/api/FlipperFileApiImpl.kt @@ -36,15 +36,19 @@ class FlipperFileApiImpl @Inject constructor( } override suspend fun insert(file: FlipperFile) { - if (file.path.fileType != FlipperFileType.SHADOW_NFC) { - error("There is support only for NFC shadow files at the moment") + val extension = when (file.path.fileType) { + FlipperFileType.SHADOW_NFC -> FlipperKeyType.NFC.extension + FlipperFileType.UI_INFRARED -> FlipperKeyType.INFRARED.extension + else -> { + error("There is support only for NFC shadow and INFRARED UI files at the moment") + } } val pathToKeyFile = FlipperFilePath( folder = file.path.folder, - nameWithExtension = "${file.path.nameWithoutExtension}.${FlipperKeyType.NFC.extension}" + nameWithExtension = "${file.path.nameWithoutExtension}.$extension" ) val foundedKey = simpleKeyDao.getByPath(pathToKeyFile.pathToKey, deleted = false) - ?: error("Can't find nfc key for ${file.path}") + ?: error("Can't find nfc or infrared key for ${file.path}") val faf = FlipperAdditionalFile( path = file.path.pathToKey, diff --git a/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/executor/AndroidKeyStorage.kt b/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/executor/AndroidKeyStorage.kt index 1e00854d27..d468c6c6a2 100644 --- a/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/executor/AndroidKeyStorage.kt +++ b/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/executor/AndroidKeyStorage.kt @@ -30,7 +30,10 @@ class AndroidKeyStorage @Inject constructor( FlipperFileType.KEY -> simpleKeyApi.getKey( FlipperKeyPath(filePath, deleted = false) )?.mainFile?.content ?: error("Can't found $filePath") - FlipperFileType.SHADOW_NFC -> flipperFileApi.getFile(filePath).content + + FlipperFileType.SHADOW_NFC, + FlipperFileType.UI_INFRARED -> flipperFileApi.getFile(filePath).content + FlipperFileType.OTHER -> error("I cannot process a file that is neither a key nor a shadow file: $filePath") } @@ -49,8 +52,11 @@ class AndroidKeyStorage @Inject constructor( ), newContent ) + + FlipperFileType.UI_INFRARED, FlipperFileType.SHADOW_NFC -> flipperFileApi.updateFileContent(filePath, newContent) + FlipperFileType.OTHER -> error("I cannot process a file that is neither a key nor a shadow file: $filePath") } @@ -69,7 +75,10 @@ class AndroidKeyStorage @Inject constructor( deleted = false ) ) + + FlipperFileType.UI_INFRARED, FlipperFileType.SHADOW_NFC -> flipperFileApi.insert(FlipperFile(filePath, keyContent)) + FlipperFileType.OTHER -> error("I cannot process a file that is neither a key nor a shadow file: $filePath") } @@ -79,7 +88,9 @@ class AndroidKeyStorage @Inject constructor( info { "Mark delete key $filePath" } when (filePath.fileType) { FlipperFileType.KEY -> deleteKeyApi.markDeleted(filePath) + FlipperFileType.UI_INFRARED, FlipperFileType.SHADOW_NFC -> flipperFileApi.deleteFile(filePath) + FlipperFileType.OTHER -> error("I cannot process a file that is neither a key nor a shadow file: $filePath") } diff --git a/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/executor/DiffKeyExecutor.kt b/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/executor/DiffKeyExecutor.kt index af0d5ed0dc..5ae03e447f 100644 --- a/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/executor/DiffKeyExecutor.kt +++ b/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/executor/DiffKeyExecutor.kt @@ -69,6 +69,7 @@ class DiffKeyExecutorImpl @Inject constructor() : DiffKeyExecutor, LogTagProvide } FlipperFileType.SHADOW_NFC -> FlipperKeyType.NFC.flipperDir + FlipperFileType.UI_INFRARED -> FlipperKeyType.INFRARED.flipperDir FlipperFileType.OTHER -> error("Don't support file with this type") } val targetPath = FlipperFilePath( diff --git a/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/repository/flipper/FlipperHashRepository.kt b/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/repository/flipper/FlipperHashRepository.kt index d9e3930547..fb6672670c 100644 --- a/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/repository/flipper/FlipperHashRepository.kt +++ b/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/repository/flipper/FlipperHashRepository.kt @@ -72,6 +72,11 @@ class FlipperHashRepositoryImpl @Inject constructor( ) { return true } + if (FlipperFileType.getByExtension(extension) == FlipperFileType.UI_INFRARED && + requestedType == FlipperKeyType.INFRARED + ) { + return true + } val fileTypeByExtension = FlipperKeyType.getByExtension(extension) if (fileTypeByExtension == null) { debug { diff --git a/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/utils/KeyDiffCombiner.kt b/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/utils/KeyDiffCombiner.kt index 4365f4f276..2aee39c4a6 100644 --- a/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/utils/KeyDiffCombiner.kt +++ b/components/bridge/synchronization/impl/src/main/java/com/flipperdevices/bridge/synchronization/impl/utils/KeyDiffCombiner.kt @@ -56,7 +56,9 @@ object KeyDiffCombiner { private fun resolveConflictBothAdd(first: KeyDiff, second: KeyDiff): KeyDiff? { return if (first.newHash.hash == second.newHash.hash) { null - } else if (first.newHash.keyPath.fileType == FlipperFileType.SHADOW_NFC) { + } else if (listOf(FlipperFileType.SHADOW_NFC, FlipperFileType.UI_INFRARED) + .contains(first.newHash.keyPath.fileType) + ) { /** * If the file is shadow, we always give priority * to a copy of the file from the flipper. diff --git a/components/infrared/impl/build.gradle.kts b/components/infrared/impl/build.gradle.kts index 7ad3a6fbb8..82951ef26e 100644 --- a/components/infrared/impl/build.gradle.kts +++ b/components/infrared/impl/build.gradle.kts @@ -21,6 +21,9 @@ dependencies { implementation(projects.components.bridge.pbutils) implementation(projects.components.bridge.synchronization.api) + implementation(projects.components.remoteControls.grid.main.api) + implementation(projects.components.remoteControls.grid.saved.api) + implementation(projects.components.core.di) implementation(projects.components.core.log) implementation(projects.components.core.data) diff --git a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/api/InfraredDecomposeComponentImpl.kt b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/api/InfraredDecomposeComponentImpl.kt index 6a470c3aa6..38ab927293 100644 --- a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/api/InfraredDecomposeComponentImpl.kt +++ b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/api/InfraredDecomposeComponentImpl.kt @@ -3,6 +3,8 @@ package com.flipperdevices.infrared.impl.api import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pushNew +import com.arkivanov.decompose.router.stack.replaceCurrent import com.arkivanov.decompose.value.Value import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath import com.flipperdevices.core.di.AppGraph @@ -10,6 +12,7 @@ import com.flipperdevices.infrared.api.InfraredDecomposeComponent import com.flipperdevices.infrared.api.InfraredEditorDecomposeComponent import com.flipperdevices.infrared.impl.model.InfraredNavigationConfig import com.flipperdevices.keyedit.api.KeyEditDecomposeComponent +import com.flipperdevices.remotecontrols.impl.grid.local.api.LocalGridScreenDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import com.flipperdevices.ui.decompose.popOr @@ -17,23 +20,26 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import me.gulya.anvil.assisted.ContributesAssistedFactory +@Suppress("LongParameterList") @ContributesAssistedFactory(AppGraph::class, InfraredDecomposeComponent.Factory::class) class InfraredDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, - @Assisted keyPath: FlipperKeyPath, + @Assisted private val keyPath: FlipperKeyPath, @Assisted private val onBack: DecomposeOnBackParameter, private val infraredViewFactory: InfraredViewDecomposeComponentImpl.Factory, private val infraredEditorFactory: InfraredEditorDecomposeComponent.Factory, - private val editorKeyFactory: KeyEditDecomposeComponent.Factory + private val editorKeyFactory: KeyEditDecomposeComponent.Factory, + private val savedGridFactory: LocalGridScreenDecomposeComponent.Factory ) : InfraredDecomposeComponent(), ComponentContext by componentContext { - override val stack: Value> = childStack( - source = navigation, - serializer = InfraredNavigationConfig.serializer(), - initialConfiguration = InfraredNavigationConfig.View(keyPath), - handleBackButton = true, - childFactory = ::child, - ) + override val stack: Value> = + childStack( + source = navigation, + serializer = InfraredNavigationConfig.serializer(), + initialConfiguration = InfraredNavigationConfig.RemoteControl(keyPath), + handleBackButton = true, + childFactory = ::child, + ) private fun child( config: InfraredNavigationConfig, @@ -58,5 +64,27 @@ class InfraredDecomposeComponentImpl @AssistedInject constructor( flipperKeyPath = config.keyPath, title = null ) + + is InfraredNavigationConfig.RemoteControl -> savedGridFactory.invoke( + componentContext = componentContext, + keyPath = keyPath, + onBack = { navigation.popOr(onBack::invoke) }, + onCallback = { + when (it) { + LocalGridScreenDecomposeComponent.Callback.UiFileNotFound -> { + navigation.replaceCurrent(InfraredNavigationConfig.View(config.keyPath)) + } + is LocalGridScreenDecomposeComponent.Callback.ViewRemoteInfo -> { + navigation.replaceCurrent(InfraredNavigationConfig.View(it.keyPath)) + } + + is LocalGridScreenDecomposeComponent.Callback.Rename -> { + navigation.pushNew(InfraredNavigationConfig.Rename(it.keyPath)) + } + + LocalGridScreenDecomposeComponent.Callback.Deleted -> navigation.popOr(onBack::invoke) + } + } + ) } } diff --git a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/model/InfraredNavigationConfig.kt b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/model/InfraredNavigationConfig.kt index dc37786bea..0de65edb00 100644 --- a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/model/InfraredNavigationConfig.kt +++ b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/model/InfraredNavigationConfig.kt @@ -13,4 +13,7 @@ sealed class InfraredNavigationConfig { @Serializable data class Rename(val keyPath: FlipperKeyPath) : InfraredNavigationConfig() + + @Serializable + data class RemoteControl(val keyPath: FlipperKeyPath) : InfraredNavigationConfig() } diff --git a/components/keyedit/api/src/main/java/com/flipperdevices/keyedit/api/KeyEditDecomposeComponent.kt b/components/keyedit/api/src/main/java/com/flipperdevices/keyedit/api/KeyEditDecomposeComponent.kt index a169e2ca12..777d1a7cc1 100644 --- a/components/keyedit/api/src/main/java/com/flipperdevices/keyedit/api/KeyEditDecomposeComponent.kt +++ b/components/keyedit/api/src/main/java/com/flipperdevices/keyedit/api/KeyEditDecomposeComponent.kt @@ -1,6 +1,7 @@ package com.flipperdevices.keyedit.api import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.dao.api.model.FlipperKey import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import com.flipperdevices.ui.decompose.ScreenDecomposeComponent @@ -12,6 +13,7 @@ abstract class KeyEditDecomposeComponent( operator fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, + onSave: (FlipperKey?) -> Unit = { onBack.invoke() }, flipperKeyPath: FlipperKeyPath, title: String? ): KeyEditDecomposeComponent @@ -19,6 +21,7 @@ abstract class KeyEditDecomposeComponent( operator fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, + onSave: (FlipperKey?) -> Unit = { onBack.invoke() }, notSavedFlipperKey: NotSavedFlipperKey, title: String? ): KeyEditDecomposeComponent diff --git a/components/keyedit/api/src/main/java/com/flipperdevices/keyedit/api/NotSavedFlipperKey.kt b/components/keyedit/api/src/main/java/com/flipperdevices/keyedit/api/NotSavedFlipperKey.kt index 1cd079236f..849181dc12 100644 --- a/components/keyedit/api/src/main/java/com/flipperdevices/keyedit/api/NotSavedFlipperKey.kt +++ b/components/keyedit/api/src/main/java/com/flipperdevices/keyedit/api/NotSavedFlipperKey.kt @@ -23,7 +23,7 @@ data class NotSavedFlipperKey( @Serializable data class NotSavedFlipperFile( val path: FlipperFilePath, - val content: FlipperKeyContent.InternalFile + val content: FlipperKeyContent ) : Parcelable suspend fun FlipperFile.toNotSavedFlipperFile( diff --git a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/api/KeyEditDecomposeComponentFactory.kt b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/api/KeyEditDecomposeComponentFactory.kt index 4221848c30..44df67302d 100644 --- a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/api/KeyEditDecomposeComponentFactory.kt +++ b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/api/KeyEditDecomposeComponentFactory.kt @@ -1,6 +1,7 @@ package com.flipperdevices.keyedit.impl.api import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.dao.api.model.FlipperKey import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath import com.flipperdevices.core.di.AppGraph import com.flipperdevices.keyedit.api.KeyEditDecomposeComponent @@ -17,20 +18,22 @@ class KeyEditDecomposeComponentFactory @Inject constructor( override fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, + onSave: (FlipperKey?) -> Unit, flipperKeyPath: FlipperKeyPath, title: String? ): KeyEditDecomposeComponent { val editableKey = EditableKey.Existed(flipperKeyPath) - return keyEditRealFactory(componentContext, onBack, editableKey, title) + return keyEditRealFactory(componentContext, onBack, onSave, editableKey, title) } override fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, + onSave: (FlipperKey?) -> Unit, notSavedFlipperKey: NotSavedFlipperKey, title: String? ): KeyEditDecomposeComponent { val editableKey = EditableKey.Limb(notSavedFlipperKey) - return keyEditRealFactory(componentContext, onBack, editableKey, title) + return keyEditRealFactory(componentContext, onBack, onSave, editableKey, title) } } diff --git a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/api/KeyEditDecomposeComponentImpl.kt b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/api/KeyEditDecomposeComponentImpl.kt index 8907aa5173..0e059099fa 100644 --- a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/api/KeyEditDecomposeComponentImpl.kt +++ b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/api/KeyEditDecomposeComponentImpl.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.dao.api.model.FlipperKey import com.flipperdevices.core.ui.lifecycle.viewModelWithFactory import com.flipperdevices.keyedit.api.KeyEditDecomposeComponent import com.flipperdevices.keyedit.impl.composable.ComposableEditScreen @@ -19,6 +20,7 @@ class KeyEditDecomposeComponentImpl @AssistedInject constructor( @Assisted private val editableKey: EditableKey, @Assisted private val title: String?, @Assisted private val onBack: DecomposeOnBackParameter, + @Assisted private val onSave: (FlipperKey?) -> Unit, private val keyEditViewModelFactory: KeyEditViewModel.Factory ) : KeyEditDecomposeComponent(componentContext) { @Composable @@ -35,7 +37,7 @@ class KeyEditDecomposeComponentImpl @AssistedInject constructor( state = state, onBack = onBack::invoke, onSave = { - viewModel.onSave(onBack::invoke) + viewModel.onSave(onSave::invoke) } ) } @@ -45,6 +47,7 @@ class KeyEditDecomposeComponentImpl @AssistedInject constructor( operator fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, + onSave: (FlipperKey?) -> Unit, editableKey: EditableKey, title: String? ): KeyEditDecomposeComponentImpl diff --git a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/KeyEditViewModel.kt b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/KeyEditViewModel.kt index 6b83cc8403..516daf75fd 100644 --- a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/KeyEditViewModel.kt +++ b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/KeyEditViewModel.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Vibrator import androidx.core.content.ContextCompat import com.flipperdevices.bridge.api.utils.FlipperSymbolFilter +import com.flipperdevices.bridge.dao.api.model.FlipperKey import com.flipperdevices.core.ktx.android.vibrateCompat import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error @@ -83,7 +84,7 @@ class KeyEditViewModel @AssistedInject constructor( } } - fun onSave(onEndAction: () -> Unit) { + fun onSave(onEndAction: (FlipperKey?) -> Unit) { val savingState = keyEditState.updateAndGet { if (it is KeyEditState.Editing) { KeyEditState.Saving(it) diff --git a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/EditableKeyProcessor.kt b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/EditableKeyProcessor.kt index 1f3fe6e3eb..d240ac4183 100644 --- a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/EditableKeyProcessor.kt +++ b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/EditableKeyProcessor.kt @@ -1,9 +1,10 @@ package com.flipperdevices.keyedit.impl.viewmodel.processors +import com.flipperdevices.bridge.dao.api.model.FlipperKey import com.flipperdevices.keyedit.impl.model.EditableKey import com.flipperdevices.keyedit.impl.model.KeyEditState interface EditableKeyProcessor { suspend fun loadKey(editableKey: T, onStateUpdate: suspend (KeyEditState) -> Unit) - suspend fun onSave(editableKey: T, editState: KeyEditState.Editing, onEndAction: () -> Unit) + suspend fun onSave(editableKey: T, editState: KeyEditState.Editing, onEndAction: (FlipperKey?) -> Unit) } diff --git a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/ExistedKeyProcessor.kt b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/ExistedKeyProcessor.kt index 9e4789a804..74fb09842a 100644 --- a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/ExistedKeyProcessor.kt +++ b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/ExistedKeyProcessor.kt @@ -3,6 +3,7 @@ package com.flipperdevices.keyedit.impl.viewmodel.processors 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.FlipperFilePath +import com.flipperdevices.bridge.dao.api.model.FlipperKey import com.flipperdevices.bridge.synchronization.api.SynchronizationApi import com.flipperdevices.keyedit.impl.model.EditableKey import com.flipperdevices.keyedit.impl.model.KeyEditState @@ -38,25 +39,30 @@ class ExistedKeyProcessor @Inject constructor( override suspend fun onSave( editableKey: EditableKey.Existed, editState: KeyEditState.Editing, - onEndAction: () -> Unit + onEndAction: (FlipperKey?) -> Unit ) { + val oldKey = simpleKeyApi.getKey(editableKey.flipperKeyPath) ?: kotlin.run { + onEndAction.invoke(null) + return + } + val extension = editableKey.flipperKeyPath.path + .nameWithExtension + .substringAfterLast('.') + val newFlipperKey = oldKey.copy( + mainFile = oldKey.mainFile.copy( + path = FlipperFilePath( + editableKey.flipperKeyPath.path.folder, + "${editState.name}.$extension" + ) + ), + notes = editState.notes + ) + try { - val oldKey = simpleKeyApi.getKey(editableKey.flipperKeyPath) ?: return - val extension = - editableKey.flipperKeyPath.path.nameWithExtension.substringAfterLast('.') - val newFlipperKey = oldKey.copy( - mainFile = oldKey.mainFile.copy( - path = FlipperFilePath( - editableKey.flipperKeyPath.path.folder, - "${editState.name}.$extension" - ) - ), - notes = editState.notes - ) updateKeyApi.updateKey(oldKey, newFlipperKey) synchronizationApi.startSynchronization(force = true) } finally { - onEndAction() + onEndAction(newFlipperKey) } } } diff --git a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/LimboKeyProcessor.kt b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/LimboKeyProcessor.kt index 351a31e323..b22ccbc361 100644 --- a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/LimboKeyProcessor.kt +++ b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/LimboKeyProcessor.kt @@ -48,7 +48,7 @@ class LimboKeyProcessor @Inject constructor( override suspend fun onSave( editableKey: EditableKey.Limb, editState: KeyEditState.Editing, - onEndAction: () -> Unit + onEndAction: (FlipperKey?) -> Unit ) { val newPathUnfree = if (editState.name != null) { editableKey.notSavedFlipperKey.mainFile.path.copyWithChangedName(editState.name) @@ -71,7 +71,7 @@ class LimboKeyProcessor @Inject constructor( descId = R.string.saved_key_desc ) ) - onEndAction() + onEndAction(newKey) } } diff --git a/components/keyedit/noop/src/main/java/com/flipperdevices/keyedit/KeyEditDecomposeComponentNoop.kt b/components/keyedit/noop/src/main/java/com/flipperdevices/keyedit/KeyEditDecomposeComponentNoop.kt index 73fe6e2052..e4d73012fd 100644 --- a/components/keyedit/noop/src/main/java/com/flipperdevices/keyedit/KeyEditDecomposeComponentNoop.kt +++ b/components/keyedit/noop/src/main/java/com/flipperdevices/keyedit/KeyEditDecomposeComponentNoop.kt @@ -2,6 +2,7 @@ package com.flipperdevices.keyedit import androidx.compose.runtime.Composable import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.dao.api.model.FlipperKey import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath import com.flipperdevices.core.di.AppGraph import com.flipperdevices.keyedit.api.KeyEditDecomposeComponent @@ -23,6 +24,7 @@ class KeyEditDecomposeComponentNoop( override fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, + onSave: (FlipperKey?) -> Unit, flipperKeyPath: FlipperKeyPath, title: String? ) = KeyEditDecomposeComponentNoop(componentContext) @@ -30,6 +32,7 @@ class KeyEditDecomposeComponentNoop( override fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, + onSave: (FlipperKey?) -> Unit, notSavedFlipperKey: NotSavedFlipperKey, title: String? ) = KeyEditDecomposeComponentNoop(componentContext) diff --git a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/BrandsScreen.kt b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/BrandsScreen.kt index a3c3761f48..f1c4cf57b3 100644 --- a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/BrandsScreen.kt +++ b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/BrandsScreen.kt @@ -11,9 +11,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable -import com.flipperdevices.ifrmvp.core.ui.layout.shared.LoadingComposable import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar import com.flipperdevices.remotecontrols.impl.brands.composable.composable.BrandsLoadedContent +import com.flipperdevices.remotecontrols.impl.brands.composable.composable.BrandsLoadingComposable import com.flipperdevices.remotecontrols.impl.brands.presentation.decompose.BrandsDecomposeComponent import com.flipperdevices.remotecontrols.brands.impl.R as BrandsR @@ -51,7 +51,7 @@ fun BrandsScreen( } BrandsDecomposeComponent.Model.Loading -> { - LoadingComposable() + BrandsLoadingComposable() } } } diff --git a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/BrandsLoadingComposable.kt b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/BrandsLoadingComposable.kt new file mode 100644 index 0000000000..3c3876b3d6 --- /dev/null +++ b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/BrandsLoadingComposable.kt @@ -0,0 +1,93 @@ +package com.flipperdevices.remotecontrols.impl.brands.composable.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.placeholderConnecting +import com.flipperdevices.core.ui.theme.FlipperThemeInternal +import com.flipperdevices.core.ui.theme.LocalPalletV2 + +private fun LazyListScope.itemsPlaceholder(count: Int) { + item { + Box( + modifier = Modifier + .width(24.dp) + .height(24.dp) + .placeholderConnecting() + ) + } + item { + Spacer(Modifier.height(12.dp)) + } + items(count) { + Box( + modifier = Modifier + .width(256.dp) + .height(20.dp) + .placeholderConnecting() + ) + Spacer(Modifier.height(8.dp)) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(LocalPalletV2.current.surface.backgroundMain.separator) + ) + Spacer(Modifier.height(8.dp)) + } +} + +@Composable +fun BrandsLoadingComposable(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(end = 14.dp) + ) { + itemsPlaceholder(count = 4) + itemsPlaceholder(count = 8) + } + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxHeight() + ) { + repeat(times = 24) { + Box( + modifier = Modifier + .padding(vertical = 4.dp) + .width(16.dp) + .height(16.dp) + .placeholderConnecting() + ) + } + } + } +} + +@Preview +@Composable +private fun BrandsLoadingComposablePreview() { + FlipperThemeInternal { + BrandsLoadingComposable() + } +} diff --git a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/composable/DeviceCategoriesScreen.kt b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/composable/DeviceCategoriesScreen.kt index a291e86161..298006b5f1 100644 --- a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/composable/DeviceCategoriesScreen.kt +++ b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/composable/DeviceCategoriesScreen.kt @@ -10,9 +10,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable -import com.flipperdevices.ifrmvp.core.ui.layout.shared.LoadingComposable import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar import com.flipperdevices.remotecontrols.impl.categories.composable.components.DeviceCategoriesLoadedContent +import com.flipperdevices.remotecontrols.impl.categories.composable.components.DeviceCategoriesLoadingContent import com.flipperdevices.remotecontrols.impl.categories.presentation.decompose.DeviceCategoriesComponent import com.flipperdevices.remotecontrols.categories.impl.R as CategoriesR @@ -48,7 +48,7 @@ internal fun DeviceCategoriesScreen( } DeviceCategoriesComponent.Model.Loading -> { - LoadingComposable() + DeviceCategoriesLoadingContent() } } } diff --git a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/composable/components/DeviceCategoriesLoadingContent.kt b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/composable/components/DeviceCategoriesLoadingContent.kt new file mode 100644 index 0000000000..8b127aae5e --- /dev/null +++ b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/composable/components/DeviceCategoriesLoadingContent.kt @@ -0,0 +1,58 @@ +package com.flipperdevices.remotecontrols.impl.categories.composable.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.placeholderConnecting +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.ifrmvp.backend.model.CategoryManifest +import com.flipperdevices.ifrmvp.backend.model.CategoryMeta +import com.flipperdevices.ifrmvp.backend.model.DeviceCategory + +@Composable +fun DeviceCategoriesLoadingContent(modifier: Modifier = Modifier) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = modifier.padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(count = 8) { + Card( + modifier = Modifier.placeholderConnecting(), + backgroundColor = LocalPalletV2.current.surface.contentCard.body.default, + shape = RoundedCornerShape(12.dp), + content = { + DeviceCategoryComposable( + onClick = {}, + deviceCategory = DeviceCategory( + id = -1, + meta = CategoryMeta( + iconPngBase64 = "", + iconSvgBase64 = "", + manifest = CategoryManifest( + displayName = "", + singularDisplayName = "" + ) + ), + ) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(92.dp) + ) + } + ) + } + } +} diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/Base64ImageButton.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/Base64ImageButton.kt index 43599f5db9..fafa900466 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/Base64ImageButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/Base64ImageButton.kt @@ -64,7 +64,6 @@ fun rememberImageBitmap(base64Image: String): ImageBitmap? { @Composable fun Base64ImageButton( base64Icon: String, - isEmulating: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit ) { @@ -76,9 +75,10 @@ fun Base64ImageButton( bitmap = imageBitmap, iconTint = Color.Unspecified, modifier = modifier, - isEmulating = isEmulating ) } else { - UnknownButton(onClick = onClick) + UnknownButton( + onClick = onClick, + ) } } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/ButtonItemComposable.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/ButtonItemComposable.kt index 29ba3e6865..f8f02f69c3 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/ButtonItemComposable.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/ButtonItemComposable.kt @@ -3,6 +3,7 @@ package com.flipperdevices.ifrmvp.core.ui.button import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.ifrmvp.core.ui.button.core.ButtonPlaceholderComposition import com.flipperdevices.ifrmvp.core.ui.button.core.SquareIconButton import com.flipperdevices.ifrmvp.core.ui.button.core.TextButton import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier @@ -21,72 +22,116 @@ fun ButtonItemComposable( buttonData: ButtonData, emulatedKeyIdentifier: IfrKeyIdentifier?, onKeyDataClick: (IfrKeyIdentifier) -> Unit, + isSyncing: Boolean, modifier: Modifier = Modifier ) { when (buttonData) { is IconButtonData -> { - SquareIconButton( - iconType = buttonData.iconId, - modifier = modifier, + ButtonPlaceholderComposition( + isSyncing = isSyncing, isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, - onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) } + content = { + SquareIconButton( + iconType = buttonData.iconId, + modifier = modifier, + onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) }, + ) + } ) } is ChannelButtonData -> { - ChannelButton( - onNextClick = { onKeyDataClick.invoke(buttonData.addKeyIdentifier) }, - onPrevClick = { onKeyDataClick.invoke(buttonData.reduceKeyIdentifier) }, - modifier = modifier, + ButtonPlaceholderComposition( + isSyncing = isSyncing, isEmulating = buttonData.reduceKeyIdentifier == emulatedKeyIdentifier || - buttonData.addKeyIdentifier == emulatedKeyIdentifier + buttonData.addKeyIdentifier == emulatedKeyIdentifier, + content = { + ChannelButton( + onNextClick = { onKeyDataClick.invoke(buttonData.addKeyIdentifier) }, + onPrevClick = { onKeyDataClick.invoke(buttonData.reduceKeyIdentifier) }, + modifier = modifier, + ) + } ) } is VolumeButtonData -> { - VolumeButton( - onAddClick = { onKeyDataClick.invoke(buttonData.addKeyIdentifier) }, - onReduceClick = { onKeyDataClick.invoke(buttonData.reduceKeyIdentifier) }, - modifier = modifier, + ButtonPlaceholderComposition( + isSyncing = isSyncing, isEmulating = buttonData.reduceKeyIdentifier == emulatedKeyIdentifier || - buttonData.addKeyIdentifier == emulatedKeyIdentifier + buttonData.addKeyIdentifier == emulatedKeyIdentifier, + content = { + VolumeButton( + onAddClick = { onKeyDataClick.invoke(buttonData.addKeyIdentifier) }, + onReduceClick = { onKeyDataClick.invoke(buttonData.reduceKeyIdentifier) }, + modifier = modifier, + ) + } ) } is NavigationButtonData -> { - NavigationButton( - onLeftClick = { onKeyDataClick.invoke(buttonData.leftKeyIdentifier) }, - onRightClick = { onKeyDataClick.invoke(buttonData.rightKeyIdentifier) }, - onDownClick = { onKeyDataClick.invoke(buttonData.downKeyIdentifier) }, - onUpClick = { onKeyDataClick.invoke(buttonData.upKeyIdentifier) }, - onOkClick = { onKeyDataClick.invoke(buttonData.okKeyIdentifier) }, - modifier = modifier, + ButtonPlaceholderComposition( + isSyncing = isSyncing, + isEmulating = listOf( + buttonData.okKeyIdentifier, + buttonData.upKeyIdentifier, + buttonData.rightKeyIdentifier, + buttonData.downKeyIdentifier, + buttonData.leftKeyIdentifier + ).contains(emulatedKeyIdentifier), + content = { + NavigationButton( + onLeftClick = { onKeyDataClick.invoke(buttonData.leftKeyIdentifier) }, + onRightClick = { onKeyDataClick.invoke(buttonData.rightKeyIdentifier) }, + onDownClick = { onKeyDataClick.invoke(buttonData.downKeyIdentifier) }, + onUpClick = { onKeyDataClick.invoke(buttonData.upKeyIdentifier) }, + onOkClick = { onKeyDataClick.invoke(buttonData.okKeyIdentifier) }, + modifier = modifier, + ) + } ) } is TextButtonData -> { - TextButton( - onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) }, - text = buttonData.text, - background = LocalPalletV2.current.surface.menu.body.dufault, + ButtonPlaceholderComposition( + isSyncing = isSyncing, isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, - modifier = modifier, + content = { + TextButton( + onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) }, + text = buttonData.text, + background = LocalPalletV2.current.surface.menu.body.dufault, + modifier = modifier, + ) + } ) } is Base64ImageButtonData -> { - Base64ImageButton( - base64Icon = buttonData.pngBase64, - onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) }, - modifier = modifier, + ButtonPlaceholderComposition( + isSyncing = isSyncing, isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, + content = { + Base64ImageButton( + base64Icon = buttonData.pngBase64, + onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) }, + modifier = modifier, + ) + } ) } UnknownButtonData -> { - UnknownButton( - modifier = modifier, - onClick = {} + ButtonPlaceholderComposition( + isSyncing = isSyncing, + isEmulating = false, + content = { + UnknownButton( + modifier = modifier, + onClick = {}, + ) + } ) } } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/DoubleButton.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/DoubleButton.kt index a11c553e23..f8eb1f28c0 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/DoubleButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/DoubleButton.kt @@ -9,6 +9,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.ifrmvp.core.ui.button.core.LocalButtonPlaceholder +import com.flipperdevices.ifrmvp.core.ui.button.core.SyncingBox import com.flipperdevices.ifrmvp.core.ui.button.core.TextButton import com.flipperdevices.ifrmvp.core.ui.layout.core.sf @@ -18,7 +20,6 @@ fun DoubleButton( onLastClick: () -> Unit, firstText: String, lastText: String, - isEmulating: Boolean, modifier: Modifier = Modifier, text: String? = null, ) { @@ -33,22 +34,20 @@ fun DoubleButton( onClick = onFirstClick, text = firstText, background = LocalPalletV2.current.surface.menu.body.dufault, - isEmulating = isEmulating, ) text?.let { TextButton( onClick = null, text = text, background = LocalPalletV2.current.surface.menu.body.dufault, - isEmulating = isEmulating ) } TextButton( onClick = onLastClick, text = lastText, background = LocalPalletV2.current.surface.menu.body.dufault, - isEmulating = isEmulating ) + SyncingBox(LocalButtonPlaceholder.current.isSyncing) } } @@ -56,7 +55,6 @@ fun DoubleButton( fun VolumeButton( onAddClick: () -> Unit, onReduceClick: () -> Unit, - isEmulating: Boolean, modifier: Modifier = Modifier ) { DoubleButton( @@ -66,7 +64,6 @@ fun VolumeButton( firstText = "+", lastText = "-", modifier = modifier, - isEmulating = isEmulating ) } @@ -74,7 +71,6 @@ fun VolumeButton( fun ChannelButton( onNextClick: () -> Unit, onPrevClick: () -> Unit, - isEmulating: Boolean, modifier: Modifier = Modifier, ) { DoubleButton( @@ -84,6 +80,5 @@ fun ChannelButton( firstText = "+", lastText = "-", modifier = modifier, - isEmulating = isEmulating ) } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/NavigationButton.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/NavigationButton.kt index 0048ff8bc2..6cd9f8db23 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/NavigationButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/NavigationButton.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.ifrmvp.core.ui.button.core.LocalButtonPlaceholder +import com.flipperdevices.ifrmvp.core.ui.button.core.SyncingBox import com.flipperdevices.ifrmvp.core.ui.layout.core.sf import com.flipperdevices.ifrmvp.core.ui.layout.core.sfp import com.flipperdevices.ifrmvp.core.ui.util.GridConstants @@ -111,5 +113,6 @@ fun NavigationButton( .clickable(onClick = onDownClick) .align(Alignment.BottomCenter) ) + SyncingBox(isSyncing = LocalButtonPlaceholder.current.isSyncing) } } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/UnknownButton.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/UnknownButton.kt index c9c042c4cd..fcd8de5c99 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/UnknownButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/UnknownButton.kt @@ -19,6 +19,6 @@ fun UnknownButton( background = LocalPalletV2.current.action.danger.background.primary.default, painter = rememberVectorPainter(Icons.Default.Error), iconTint = MaterialTheme.colors.onPrimary, - modifier = modifier + modifier = modifier, ) } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/LocalButtonPlaceholder.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/LocalButtonPlaceholder.kt new file mode 100644 index 0000000000..a37e961db6 --- /dev/null +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/LocalButtonPlaceholder.kt @@ -0,0 +1,34 @@ +package com.flipperdevices.ifrmvp.core.ui.button.core + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf + +@Suppress("CompositionLocalAllowlist") +val LocalButtonPlaceholder = compositionLocalOf { ButtonPlaceholderState.NONE } + +enum class ButtonPlaceholderState { + NONE, SYNCING, EMULATING; + + val isEmulating: Boolean + get() = this == EMULATING + val isSyncing: Boolean + get() = this == SYNCING +} + +@Composable +internal fun ButtonPlaceholderComposition( + isSyncing: Boolean, + isEmulating: Boolean, + content: @Composable () -> Unit +) { + val buttonPlaceholderState = when { + isSyncing -> ButtonPlaceholderState.SYNCING + isEmulating -> ButtonPlaceholderState.EMULATING + else -> ButtonPlaceholderState.NONE + } + CompositionLocalProvider( + value = LocalButtonPlaceholder provides buttonPlaceholderState, + content = content + ) +} diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/SquareButton.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/SquareButton.kt index 7e38c4cd03..aa28eb9d8b 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/SquareButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/SquareButton.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import com.flipperdevices.core.ui.ktx.placeholderConnecting +import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.ifrmvp.core.ui.layout.core.sf import com.flipperdevices.ifrmvp.core.ui.util.GridConstants @@ -21,7 +22,6 @@ import com.flipperdevices.ifrmvp.core.ui.util.GridConstants fun SquareButton( onClick: (() -> Unit)?, background: Color, - isEmulating: Boolean, modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { @@ -34,7 +34,7 @@ fun SquareButton( if (onClick != null) { Modifier.clickable( onClick = onClick, - enabled = !isEmulating + enabled = !LocalButtonPlaceholder.current.isEmulating ) } else { Modifier @@ -43,15 +43,48 @@ fun SquareButton( contentAlignment = Alignment.Center, content = { content.invoke(this) - Crossfade(isEmulating) { isEmulating -> - if (isEmulating) { - Box( - Modifier - .fillMaxSize() - .placeholderConnecting() - ) - } - } + EmulatingBox(isEmulating = LocalButtonPlaceholder.current.isEmulating) + SyncingBox(isSyncing = LocalButtonPlaceholder.current.isSyncing) } ) } + +@Composable +internal fun EmulatingBox( + isEmulating: Boolean, + modifier: Modifier = Modifier +) { + Crossfade(isEmulating) { isEmulatingLocal -> + if (isEmulatingLocal) { + Box( + modifier + .fillMaxSize() + .placeholderConnecting() + ) + } + } +} + +@Composable +internal fun SyncingBox( + isSyncing: Boolean, + modifier: Modifier = Modifier +) { + Crossfade( + targetState = isSyncing, + modifier = modifier + ) { isEmulating -> + if (isEmulating) { + Box( + Modifier + .fillMaxSize() + .background(LocalPalletV2.current.surface.menu.body.dufault) + ) + Box( + Modifier + .fillMaxSize() + .placeholderConnecting() + ) + } + } +} diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/SquareIconButton.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/SquareIconButton.kt index 998b60d4e8..e48c50d5a1 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/SquareIconButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/SquareIconButton.kt @@ -24,14 +24,12 @@ fun SquareIconButton( background: Color, iconTint: Color, modifier: Modifier = Modifier, - isEmulating: Boolean = false, contentDescription: String? = null, ) { SquareButton( modifier = modifier, onClick = onClick, background = background, - isEmulating = isEmulating ) { Icon( painter = painter, @@ -51,14 +49,12 @@ fun SquareImageButton( background: Color, iconTint: Color, modifier: Modifier = Modifier, - isEmulating: Boolean = false, contentDescription: String? = null, ) { SquareButton( modifier = modifier, onClick = onClick, background = background, - isEmulating = isEmulating ) { Icon( bitmap = bitmap, @@ -74,7 +70,6 @@ fun SquareImageButton( @Composable fun SquareIconButton( iconType: IconButtonData.IconType, - isEmulating: Boolean, modifier: Modifier = Modifier, contentDescription: String? = null, onClick: () -> Unit, @@ -82,7 +77,6 @@ fun SquareIconButton( SquareButton( modifier = modifier, onClick = onClick, - isEmulating = isEmulating, background = LocalPalletV2.current.surface.menu.body.dufault ) { Icon( @@ -102,8 +96,7 @@ private fun SquareIconButtonPreview() { FlipperThemeInternal { SquareIconButton( iconType = IconButtonData.IconType.POWER, - isEmulating = true, - onClick = {} + onClick = {}, ) } } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/TextButton.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/TextButton.kt index 0c439004db..e04a79d8e3 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/TextButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/TextButton.kt @@ -12,7 +12,6 @@ import com.flipperdevices.ifrmvp.core.ui.layout.core.sfp @Composable fun TextButton( text: String, - isEmulating: Boolean, modifier: Modifier = Modifier, background: Color = MaterialTheme.colors.primaryVariant, textColor: Color = MaterialTheme.colors.onPrimary, @@ -22,7 +21,6 @@ fun TextButton( onClick = onClick, background = background, modifier = modifier, - isEmulating = isEmulating, content = { Text( text = text, diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/ButtonsComposable.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/ButtonsComposable.kt similarity index 87% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/ButtonsComposable.kt rename to components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/ButtonsComposable.kt index a20e56276a..bef3d6cb04 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/ButtonsComposable.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/ButtonsComposable.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.composable.components +package com.flipperdevices.ifrmvp.core.ui.layout.shared import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraintsScope @@ -15,24 +15,24 @@ import com.flipperdevices.ifrmvp.core.ui.layout.core.GridItemComposable import com.flipperdevices.ifrmvp.core.ui.layout.core.LocalScaleFactor import com.flipperdevices.ifrmvp.core.ui.layout.core.rememberScaleFactor import com.flipperdevices.ifrmvp.core.ui.layout.core.sf -import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable import com.flipperdevices.ifrmvp.core.ui.util.GridConstants import com.flipperdevices.ifrmvp.model.IfrButton import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.ifrmvp.model.PageLayout -import com.flipperdevices.remotecontrols.grid.impl.R as GridR +import com.flipperdevices.remotecontrols.core.ui.R as GridUiR @Composable -fun BoxWithConstraintsScope.ButtonsComposable( +internal fun BoxWithConstraintsScope.ButtonsComposable( pageLayout: PageLayout?, emulatedKeyIdentifier: IfrKeyIdentifier?, + isSyncing: Boolean, onButtonClick: (IfrButton, IfrKeyIdentifier) -> Unit, - onReload: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onReload: (() -> Unit)? = null, ) { if (pageLayout?.buttons.isNullOrEmpty()) { ErrorComposable( - desc = stringResource(GridR.string.empty_page), + desc = stringResource(GridUiR.string.empty_page), onReload = onReload ) } @@ -59,6 +59,7 @@ fun BoxWithConstraintsScope.ButtonsComposable( ButtonItemComposable( buttonData = button.data, emulatedKeyIdentifier = emulatedKeyIdentifier, + isSyncing = isSyncing, onKeyDataClick = { keyIdentifier -> onButtonClick.invoke(button, keyIdentifier) } diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableLoadedContent.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/GridPagesContent.kt similarity index 81% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableLoadedContent.kt rename to components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/GridPagesContent.kt index c495350ea3..9fce83a6e3 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableLoadedContent.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/GridPagesContent.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.composable.components +package com.flipperdevices.ifrmvp.core.ui.layout.shared import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize @@ -12,12 +12,13 @@ import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.ifrmvp.model.PagesLayout @Composable -internal fun GridComposableLoadedContent( +fun GridPagesContent( pagesLayout: PagesLayout, onButtonClick: (IfrButton, IfrKeyIdentifier) -> Unit, - onReload: () -> Unit, emulatedKeyIdentifier: IfrKeyIdentifier?, + isSyncing: Boolean, modifier: Modifier = Modifier, + onReload: (() -> Unit)? = null, ) { BoxWithConstraints( modifier = modifier.fillMaxSize(), @@ -27,7 +28,8 @@ internal fun GridComposableLoadedContent( pageLayout = pagesLayout.pages.firstOrNull(), emulatedKeyIdentifier = emulatedKeyIdentifier, onButtonClick = onButtonClick, - onReload = onReload + onReload = onReload, + isSyncing = isSyncing ) } ) @@ -40,11 +42,12 @@ internal fun GridComposableLoadedContent( @Composable private fun LoadedContentEmptyPreview() { FlipperThemeInternal { - GridComposableLoadedContent( + GridPagesContent( pagesLayout = PagesLayout(emptyList()), onButtonClick = { _, _ -> }, onReload = {}, - emulatedKeyIdentifier = null + emulatedKeyIdentifier = null, + isSyncing = false ) } } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/SharedTopBar.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/SharedTopBar.kt index b1401d2758..6386cc6bef 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/SharedTopBar.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/SharedTopBar.kt @@ -3,6 +3,7 @@ package com.flipperdevices.ifrmvp.core.ui.layout.shared import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -31,7 +32,7 @@ fun SharedTopBar( modifier: Modifier = Modifier, title: String = "", subtitle: String = "", - actions: @Composable () -> Unit = {} + actions: @Composable BoxScope.() -> Unit = {} ) { Row( modifier = modifier @@ -81,7 +82,7 @@ fun SharedTopBar( modifier = Modifier.weight(weight = 1f), contentAlignment = Alignment.CenterEnd, content = { - actions.invoke() + actions.invoke(this) } ) } @@ -121,17 +122,23 @@ private fun SharedTopBarPreview() { subtitle = "Subtitle", onBackClick = {}, actions = { - Row(modifier = Modifier.weight(1f)) { - repeat(2) { - Text( - text = "Action", - color = LocalPalletV2.current.text.title.blackOnColor, - style = LocalTypography.current.titleEB18, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } + Row(modifier = Modifier) { + Text( + text = "Action", + color = LocalPalletV2.current.text.title.blackOnColor, + style = LocalTypography.current.titleEB18, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Icon( + modifier = Modifier + .size(24.dp) + .clickableRipple(bounded = false, onClick = {}), + painter = painterResource(DesignSystem.drawable.ic_back), + contentDescription = null, + tint = LocalPalletV2.current.icon.blackAndWhite.blackOnColor + ) } } ) diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/KitchenLayoutFactory.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/KitchenLayoutFactory.kt similarity index 99% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/KitchenLayoutFactory.kt rename to components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/KitchenLayoutFactory.kt index ccad35d3a5..7372003bcd 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/KitchenLayoutFactory.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/KitchenLayoutFactory.kt @@ -1,6 +1,6 @@ @file:Suppress("MagicNumber") -package com.flipperdevices.remotecontrols.impl.grid.composable.preview +package com.flipperdevices.ifrmvp.core.ui.preview import com.flipperdevices.ifrmvp.model.IfrButton import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/LoadedContentPreview.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/LoadedContentPreview.kt similarity index 63% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/LoadedContentPreview.kt rename to components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/LoadedContentPreview.kt index fb1ea453cb..867bf6f614 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/LoadedContentPreview.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/LoadedContentPreview.kt @@ -1,9 +1,9 @@ -package com.flipperdevices.remotecontrols.impl.grid.composable.preview +package com.flipperdevices.ifrmvp.core.ui.preview import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.flipperdevices.core.ui.theme.FlipperThemeInternal -import com.flipperdevices.remotecontrols.impl.grid.composable.components.GridComposableLoadedContent +import com.flipperdevices.ifrmvp.core.ui.layout.shared.GridPagesContent @Preview( showSystemUi = true, @@ -12,11 +12,12 @@ import com.flipperdevices.remotecontrols.impl.grid.composable.components.GridCom @Composable private fun LoadedContentPreview() { FlipperThemeInternal { - GridComposableLoadedContent( + GridPagesContent( pagesLayout = KitchenLayoutFactory.create(), onButtonClick = { _, _ -> }, onReload = {}, - emulatedKeyIdentifier = null + emulatedKeyIdentifier = null, + isSyncing = false ) } } diff --git a/components/remote-controls/core-ui/src/main/res/values/strings.xml b/components/remote-controls/core-ui/src/main/res/values/strings.xml index 34dba478fa..a459a99ec8 100644 --- a/components/remote-controls/core-ui/src/main/res/values/strings.xml +++ b/components/remote-controls/core-ui/src/main/res/values/strings.xml @@ -3,4 +3,5 @@ Error Reload + Page is empty! \ No newline at end of file diff --git a/components/remote-controls/grid/create-control/api/build.gradle.kts b/components/remote-controls/grid/create-control/api/build.gradle.kts new file mode 100644 index 0000000000..2d877473e5 --- /dev/null +++ b/components/remote-controls/grid/create-control/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("flipper.android-compose") +} + +android.namespace = "com.flipperdevices.remotecontrols.grid.createcontrol.api" + +dependencies { + implementation(projects.components.bridge.dao.api) + + implementation(projects.components.core.ui.decompose) + implementation(projects.components.keyedit.api) + + implementation(libs.compose.ui) + implementation(libs.decompose) +} diff --git a/components/remote-controls/grid/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/GridScreenDecomposeComponent.kt b/components/remote-controls/grid/create-control/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/CreateControlDecomposeComponent.kt similarity index 53% rename from components/remote-controls/grid/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/GridScreenDecomposeComponent.kt rename to components/remote-controls/grid/create-control/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/CreateControlDecomposeComponent.kt index ce873335e1..56a5041df8 100644 --- a/components/remote-controls/grid/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/GridScreenDecomposeComponent.kt +++ b/components/remote-controls/grid/create-control/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/CreateControlDecomposeComponent.kt @@ -2,25 +2,19 @@ package com.flipperdevices.remotecontrols.api import com.arkivanov.decompose.ComponentContext import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import com.flipperdevices.ui.decompose.ScreenDecomposeComponent -abstract class GridScreenDecomposeComponent( +abstract class CreateControlDecomposeComponent( componentContext: ComponentContext ) : ScreenDecomposeComponent(componentContext) { - fun interface Factory { operator fun invoke( componentContext: ComponentContext, - param: Param, - onPopClick: () -> Unit - ): GridScreenDecomposeComponent - } - - sealed interface Param { - data class Id(val irFileId: Long) : Param - data class Path(val flipperKeyPath: FlipperKeyPath) : Param - - val key: String - get() = this.toString() + savedKey: FlipperKeyPath, + originalKey: NotSavedFlipperKey, + onBack: DecomposeOnBackParameter + ): CreateControlDecomposeComponent } } diff --git a/components/remote-controls/grid/create-control/impl/build.gradle.kts b/components/remote-controls/grid/create-control/impl/build.gradle.kts new file mode 100644 index 0000000000..1a8b0a8655 --- /dev/null +++ b/components/remote-controls/grid/create-control/impl/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("flipper.android-compose") + id("flipper.anvil") + id("kotlinx-serialization") +} +android.namespace = "com.flipperdevices.remotecontrols.grid.createcontrol.impl" + +dependencies { + implementation(projects.components.core.di) + implementation(projects.components.core.ktx) + implementation(projects.components.core.log) + + implementation(projects.components.core.ui.lifecycle) + implementation(projects.components.core.ui.theme) + implementation(projects.components.core.ui.decompose) + implementation(projects.components.core.ui.ktx) + implementation(projects.components.core.ui.res) + + implementation(projects.components.bridge.dao.api) + implementation(projects.components.bridge.rpc.api) + implementation(projects.components.bridge.service.api) + implementation(projects.components.bridge.api) + implementation(projects.components.bridge.pbutils) + implementation(projects.components.bridge.synchronization.api) + implementation(projects.components.rootscreen.api) + + implementation(projects.components.remoteControls.grid.createControl.api) + implementation(projects.components.keyedit.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.tooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + + implementation(libs.kotlin.immutable.collections) + + implementation(libs.decompose) + + implementation(libs.bundles.decompose) +} 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 new file mode 100644 index 0000000000..fe40cd0cd5 --- /dev/null +++ b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt @@ -0,0 +1,55 @@ +package com.flipperdevices.remotecontrols.impl.createcontrol.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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 + +@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 + ) + } +} + +@Preview +@Composable +private fun CreateControlComposablePreview() { + FlipperThemeInternal { + CreateControlComposable() + } +} 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 new file mode 100644 index 0000000000..271e9e979b --- /dev/null +++ b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/decompose/CreateControlDecomposeComponentImpl.kt @@ -0,0 +1,64 @@ +package com.flipperdevices.remotecontrols.impl.createcontrol.decompose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.ui.lifecycle.viewModelWithFactory +import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import com.flipperdevices.remotecontrols.api.CreateControlDecomposeComponent +import com.flipperdevices.remotecontrols.impl.createcontrol.composable.CreateControlComposable +import com.flipperdevices.remotecontrols.impl.createcontrol.viewmodel.SaveRemoteControlViewModel +import com.flipperdevices.rootscreen.api.LocalRootNavigation +import com.flipperdevices.rootscreen.model.RootScreenConfig +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import me.gulya.anvil.assisted.ContributesAssistedFactory +import javax.inject.Provider + +@ContributesAssistedFactory(AppGraph::class, CreateControlDecomposeComponent.Factory::class) +class CreateControlDecomposeComponentImpl @AssistedInject constructor( + @Assisted private val componentContext: ComponentContext, + @Assisted private val savedKey: FlipperKeyPath, + @Assisted private val originalKey: NotSavedFlipperKey, + @Assisted private val onBack: DecomposeOnBackParameter, + private val saveRemoteControlViewModelFactory: Provider +) : CreateControlDecomposeComponent(componentContext) { + + @Composable + override fun Render() { + val saveRemoteControlViewModel = viewModelWithFactory( + key = null, + factory = { saveRemoteControlViewModelFactory.get() } + ) + val rootNavigation = LocalRootNavigation.current + LaunchedEffect(saveRemoteControlViewModel) { + saveRemoteControlViewModel.state + .onEach { + when (it) { + is SaveRemoteControlViewModel.State.Finished -> { + onBack.invoke() + rootNavigation.push(RootScreenConfig.OpenKey(it.keyPath)) + } + + SaveRemoteControlViewModel.State.CouldNotModifyFiles, + SaveRemoteControlViewModel.State.KeyNotFound -> { + onBack.invoke() + } + + SaveRemoteControlViewModel.State.Pending, + SaveRemoteControlViewModel.State.Updating -> Unit + } + }.launchIn(this) + saveRemoteControlViewModel.moveAndUpdate( + savedKeyPath = savedKey, + originalKey = originalKey, + ) + } + CreateControlComposable() + } +} 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 new file mode 100644 index 0000000000..0aee2de2ff --- /dev/null +++ b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/viewmodel/SaveRemoteControlViewModel.kt @@ -0,0 +1,171 @@ +package com.flipperdevices.remotecontrols.impl.createcontrol.viewmodel + +import com.flipperdevices.bridge.api.model.wrapToRequest +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.FlipperFile +import com.flipperdevices.bridge.dao.api.model.FlipperFilePath +import com.flipperdevices.bridge.dao.api.model.FlipperKey +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +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 +import com.flipperdevices.protobuf.Flipper +import com.flipperdevices.protobuf.main +import com.flipperdevices.protobuf.storage.deleteRequest +import com.flipperdevices.protobuf.storage.renameRequest +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import javax.inject.Inject + +class SaveRemoteControlViewModel @Inject constructor( + private val flipperServiceProvider: FlipperServiceProvider, + private val updateKeyApi: UpdateKeyApi, + private val simpleKeyApi: SimpleKeyApi, + private val synchronizationApi: SynchronizationApi +) : DecomposeViewModel(), LogTagProvider { + override val TAG: String = "SaveRemoteControlViewModel" + + private val _state = MutableStateFlow(State.Pending) + val state = _state.asStateFlow() + + private var lastMoveJob: Job? = null + + private fun FlipperFilePath.toNonTempPath() = copy(folder = folder.replace("/temp", "")) + + private suspend fun delete(path: FlipperFilePath) { + val serviceApi = flipperServiceProvider.getServiceApi() + val moveResponse = serviceApi.requestApi.request( + command = main { + storageDeleteRequest = deleteRequest { + this.path = path.getPathOnFlipper() + } + }.wrapToRequest() + ).first() + if (moveResponse.commandStatus != Flipper.CommandStatus.OK) { + error("Could not delete file ${path.getPathOnFlipper()} status: ${moveResponse.commandStatus}") + } + } + + private suspend fun move( + oldPath: FlipperFilePath, + newPath: FlipperFilePath, + ) { + val serviceApi = flipperServiceProvider.getServiceApi() + val moveResponse = serviceApi.requestApi.request( + command = main { + storageRenameRequest = renameRequest { + this.oldPath = oldPath.getPathOnFlipper() + this.newPath = newPath.getPathOnFlipper() + } + }.wrapToRequest() + ).first() + if (moveResponse.commandStatus != Flipper.CommandStatus.OK) { + error("Could not move file ${oldPath.getPathOnFlipper()} status: ${moveResponse.commandStatus}") + } + } + + private suspend fun awaitSynchronization() { + if (!synchronizationApi.isSynchronizationRunning()) { + synchronizationApi.startSynchronization(force = true) + } + synchronizationApi.getSynchronizationState() + .onEach { info { "#moveAndUpdate $it" } } + .filterIsInstance() + .first() + synchronizationApi.getSynchronizationState() + .onEach { info { "#moveAndUpdate $it" } } + .filterIsInstance() + .first() + } + + /** + * Move files to new location and delete temp files + * @return true if successful false if failure met + */ + private suspend fun tryModifyFiles( + originalKey: NotSavedFlipperKey, + flipperKey: FlipperKey + ): Boolean { + val result = runCatching { + move( + oldPath = originalKey.mainFile.path, + newPath = flipperKey.mainFile.path.toNonTempPath() + ) + delete(originalKey.mainFile.path) + originalKey.additionalFiles + .map(NotSavedFlipperFile::path) + .forEach { path -> + move( + oldPath = path, + newPath = path.toNonTempPath() + .copyWithChangedName(flipperKey.mainFile.path.nameWithoutExtension) + ) + delete(path) + } + } + return result.isSuccess + } + + fun moveAndUpdate( + savedKeyPath: FlipperKeyPath, + originalKey: NotSavedFlipperKey, + ) { + viewModelScope.launch { + _state.emit(State.Updating) + if (lastMoveJob != null) lastMoveJob?.join() + lastMoveJob = coroutineContext.job + + awaitSynchronization() + + val flipperKey = simpleKeyApi.getKey(savedKeyPath) ?: run { + _state.emit(State.KeyNotFound) + return@launch + } + if (!tryModifyFiles(originalKey, flipperKey)) { + _state.emit(State.CouldNotModifyFiles) + return@launch + } + + updateKeyApi.updateKey( + oldKey = flipperKey, + newKey = flipperKey.copy( + mainFile = flipperKey.mainFile.copy( + path = flipperKey.mainFile.path.toNonTempPath() + ), + additionalFiles = originalKey.additionalFiles.map { + FlipperFile( + path = it.path.toNonTempPath(), + content = it.content + ) + } + ) + ) + awaitSynchronization() + val keyPath = FlipperKeyPath( + path = flipperKey.mainFile.path.toNonTempPath(), + deleted = false + ) + _state.emit(State.Finished(keyPath)) + } + } + + sealed interface State { + data object Pending : State + data object Updating : State + 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 new file mode 100644 index 0000000000..38349202ed --- /dev/null +++ b/components/remote-controls/grid/create-control/impl/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + Configuring + Remote control is being configured. Please do not close this screen + \ No newline at end of file diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/GridComposable.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/GridComposable.kt deleted file mode 100644 index fc575fb54b..0000000000 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/GridComposable.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.flipperdevices.remotecontrols.impl.grid.composable - -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Scaffold -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar -import com.flipperdevices.remotecontrols.impl.grid.composable.components.GridComposableContent -import com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.GridComponent - -@Composable -fun GridComposable( - gridComponent: GridComponent, - modifier: Modifier = Modifier -) { - val coroutineScope = rememberCoroutineScope() - val scaffoldState = rememberScaffoldState() - val model by remember(gridComponent, coroutineScope) { - gridComponent.model(coroutineScope) - }.collectAsState() - Scaffold( - modifier = modifier, - topBar = { - SharedTopBar( - onBackClick = gridComponent::pop, - ) - }, - backgroundColor = LocalPalletV2.current.surface.backgroundMain.body, - scaffoldState = scaffoldState, - content = { scaffoldPaddings -> - GridComposableContent( - gridComponent = gridComponent, - model = model, - modifier = Modifier.padding(scaffoldPaddings) - ) - } - ) -} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/util/GridComponentModelExt.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/util/GridComponentModelExt.kt deleted file mode 100644 index 8444d20112..0000000000 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/util/GridComponentModelExt.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.flipperdevices.remotecontrols.impl.grid.composable.util - -import com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.GridComponent - -internal val GridComponent.Model.contentKey: Any - get() = when (this) { - GridComponent.Model.Error -> 0 - is GridComponent.Model.Loaded -> 1 - is GridComponent.Model.Loading -> 2 - } diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepository.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepository.kt deleted file mode 100644 index f36aba3d5b..0000000000 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.data.localpages - -import com.flipperdevices.bridge.dao.api.model.FlipperFilePath -import com.flipperdevices.bridge.dao.api.model.FlipperKey -import com.flipperdevices.ifrmvp.model.PagesLayout - -interface LocalPagesRepository { - suspend fun getLocalFlipperKey(path: FlipperFilePath): FlipperKey? - suspend fun getLocalPagesLayout( - path: FlipperFilePath, - toPagesLayout: (String) -> PagesLayout? - ): PagesLayout? -} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepositoryImpl.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepositoryImpl.kt deleted file mode 100644 index 75842e5ba3..0000000000 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepositoryImpl.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.data.localpages - -import com.flipperdevices.bridge.dao.api.delegates.key.SimpleKeyApi -import com.flipperdevices.bridge.dao.api.model.FlipperFilePath -import com.flipperdevices.bridge.dao.api.model.FlipperKey -import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath -import com.flipperdevices.core.di.AppGraph -import com.flipperdevices.ifrmvp.model.PagesLayout -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject - -@ContributesBinding(AppGraph::class, LocalPagesRepository::class) -class LocalPagesRepositoryImpl @Inject constructor( - private val simpleKeyApi: SimpleKeyApi, -) : LocalPagesRepository { - - override suspend fun getLocalFlipperKey(path: FlipperFilePath): FlipperKey? { - return simpleKeyApi.getKey( - FlipperKeyPath( - path = path, - deleted = false - ) - ) - } - - override suspend fun getLocalPagesLayout( - path: FlipperFilePath, - toPagesLayout: (String) -> PagesLayout? - ): PagesLayout? { - val flipperKey = getLocalFlipperKey(path) - return flipperKey - .let { it?.additionalFiles.orEmpty() } - .let { additionalFiles -> - additionalFiles.firstNotNullOfOrNull { fFile -> - val text = fFile.content.openStream().reader().readText() - toPagesLayout.invoke(text) - } - } - } -} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/internal/GridComponentImpl.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/internal/GridComponentImpl.kt deleted file mode 100644 index 56a3a58872..0000000000 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/internal/GridComponentImpl.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.internal - -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.essenty.instancekeeper.getOrCreate -import com.flipperdevices.bridge.dao.api.model.FlipperFilePath -import com.flipperdevices.core.di.AppGraph -import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier -import com.flipperdevices.remotecontrols.api.DispatchSignalApi -import com.flipperdevices.remotecontrols.api.GridScreenDecomposeComponent -import com.flipperdevices.remotecontrols.api.SaveTempSignalApi -import com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.GridComponent -import com.flipperdevices.remotecontrols.impl.grid.presentation.mapping.GridComponentStateMapper -import com.flipperdevices.remotecontrols.impl.grid.presentation.util.GridParamExt.extFolderPath -import com.flipperdevices.remotecontrols.impl.grid.presentation.util.GridParamExt.irFileIdOrNull -import com.flipperdevices.remotecontrols.impl.grid.presentation.util.GridParamExt.nameWithExtension -import com.flipperdevices.remotecontrols.impl.grid.presentation.viewmodel.GridViewModel -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import me.gulya.anvil.assisted.ContributesAssistedFactory -import javax.inject.Provider - -@ContributesAssistedFactory(AppGraph::class, GridComponent.Factory::class) -class GridComponentImpl @AssistedInject constructor( - @Assisted componentContext: ComponentContext, - @Assisted private val param: GridScreenDecomposeComponent.Param, - @Assisted private val onPopClick: () -> Unit, - createGridViewModel: GridViewModel.Factory, - createSaveTempSignalApi: Provider, - createDispatchSignalApi: Provider -) : GridComponent, ComponentContext by componentContext { - private val saveTempSignalApi = instanceKeeper.getOrCreate( - key = "GridComponent_saveSignalViewModel_${param.key}", - factory = { - createSaveTempSignalApi.get() - } - ) - private val dispatchSignalApi = instanceKeeper.getOrCreate( - key = "GridComponent_dispatchSignalViewModel_${param.key}", - factory = { - createDispatchSignalApi.get() - } - ) - private val gridViewModel = instanceKeeper.getOrCreate( - key = "GridComponent_gridFeature_${param.key}", - factory = { - createGridViewModel.invoke( - param = param, - onCallback = { callback -> - when (callback) { - is GridViewModel.Callback.InfraredFileLoaded -> { - param.irFileIdOrNull ?: return@invoke - saveTempSignalApi.saveFile( - textContent = callback.content, - nameWithExtension = param.nameWithExtension, - extFolderPath = param.extFolderPath - ) - } - - is GridViewModel.Callback.UiLoaded -> { - val id = param.irFileIdOrNull ?: return@invoke - saveTempSignalApi.saveFile( - textContent = callback.content, - nameWithExtension = "$id.ui.json", - extFolderPath = param.extFolderPath - ) - } - } - } - ) - } - ) - - override fun model(coroutineScope: CoroutineScope) = combine( - saveTempSignalApi.state, - gridViewModel.state, - dispatchSignalApi.state, - transform = { saveState, gridState, dispatchState -> - GridComponentStateMapper.map( - saveState = saveState, - gridState = gridState, - dispatchState = dispatchState - ) - } - ).stateIn(coroutineScope, SharingStarted.Eagerly, GridComponent.Model.Loading()) - - override fun dismissBusyDialog() { - dispatchSignalApi.dismissBusyDialog() - } - - override fun onButtonClick(identifier: IfrKeyIdentifier) { - val gridLoadedState = (gridViewModel.state.value as? GridViewModel.State.Loaded) ?: return - val remotes = gridLoadedState.remotes - dispatchSignalApi.dispatch( - identifier = identifier, - remotes = remotes, - ffPath = FlipperFilePath( - folder = param.extFolderPath, - nameWithExtension = param.nameWithExtension - ) - ) - } - - override fun tryLoad() = gridViewModel.tryLoad() - override fun pop() = onPopClick.invoke() -} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/internal/GridScreenDecomposeComponentImpl.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/internal/GridScreenDecomposeComponentImpl.kt deleted file mode 100644 index 3f3e00a198..0000000000 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/internal/GridScreenDecomposeComponentImpl.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.internal - -import androidx.compose.runtime.Composable -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.childContext -import com.flipperdevices.core.di.AppGraph -import com.flipperdevices.remotecontrols.api.GridScreenDecomposeComponent -import com.flipperdevices.remotecontrols.impl.grid.composable.GridComposable -import com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.GridComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import me.gulya.anvil.assisted.ContributesAssistedFactory - -@ContributesAssistedFactory(AppGraph::class, GridScreenDecomposeComponent.Factory::class) -class GridScreenDecomposeComponentImpl @AssistedInject constructor( - @Assisted componentContext: ComponentContext, - @Assisted param: GridScreenDecomposeComponent.Param, - @Assisted onPopClick: () -> Unit, - gridComponentFactory: GridComponent.Factory -) : GridScreenDecomposeComponent(componentContext) { - private val gridComponent = gridComponentFactory.invoke( - componentContext = childContext("GridComponent"), - param = param, - onPopClick = onPopClick - ) - - @Composable - override fun Render() { - GridComposable(gridComponent) - } -} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/mapping/GridComponentStateMapper.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/mapping/GridComponentStateMapper.kt deleted file mode 100644 index eaf8eebb07..0000000000 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/mapping/GridComponentStateMapper.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.mapping - -import com.flipperdevices.remotecontrols.api.DispatchSignalApi -import com.flipperdevices.remotecontrols.api.SaveTempSignalApi -import com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.GridComponent -import com.flipperdevices.remotecontrols.impl.grid.presentation.viewmodel.GridViewModel - -internal object GridComponentStateMapper { - fun map( - saveState: SaveTempSignalApi.State, - gridState: GridViewModel.State, - dispatchState: DispatchSignalApi.State - ): GridComponent.Model = when (gridState) { - GridViewModel.State.Error -> GridComponent.Model.Error - is GridViewModel.State.Loaded -> { - when (saveState) { - SaveTempSignalApi.State.Error -> GridComponent.Model.Error - SaveTempSignalApi.State.Uploaded, SaveTempSignalApi.State.Pending -> { - GridComponent.Model.Loaded( - pagesLayout = gridState.pagesLayout, - remotes = gridState.remotes, - isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, - emulatedKey = (dispatchState as? DispatchSignalApi.State.Emulating)?.ifrKeyIdentifier, - ) - } - - is SaveTempSignalApi.State.Uploading -> GridComponent.Model.Loading( - saveState.progress - ) - } - } - - GridViewModel.State.Loading -> GridComponent.Model.Loading(0f) - } -} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/util/GridParamExt.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/util/GridParamExt.kt deleted file mode 100644 index b445c93875..0000000000 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/util/GridParamExt.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.util - -import com.flipperdevices.bridge.dao.api.model.FlipperFilePath -import com.flipperdevices.bridge.dao.api.model.FlipperKeyType -import com.flipperdevices.remotecontrols.api.GridScreenDecomposeComponent - -object GridParamExt { - val GridScreenDecomposeComponent.Param.extFolderPath: String - get() = when (this) { - is GridScreenDecomposeComponent.Param.Id -> "${FlipperKeyType.INFRARED.flipperDir}/temp/" - is GridScreenDecomposeComponent.Param.Path -> "/${flipperKeyPath.path.folder}" - } - - val GridScreenDecomposeComponent.Param.nameWithExtension: String - get() = when (this) { - is GridScreenDecomposeComponent.Param.Id -> "$irFileId.ir" - is GridScreenDecomposeComponent.Param.Path -> flipperKeyPath.path.nameWithExtension - } - - val GridScreenDecomposeComponent.Param.flipperFilePath: FlipperFilePath - get() = FlipperFilePath( - folder = extFolderPath, - nameWithExtension = nameWithExtension - ) - - val GridScreenDecomposeComponent.Param.irFileIdOrNull: Long? - get() = when (this) { - is GridScreenDecomposeComponent.Param.Id -> irFileId - is GridScreenDecomposeComponent.Param.Path -> null - } -} diff --git a/components/remote-controls/grid/api/build.gradle.kts b/components/remote-controls/grid/main/api/build.gradle.kts similarity index 78% rename from components/remote-controls/grid/api/build.gradle.kts rename to components/remote-controls/grid/main/api/build.gradle.kts index 3121b13492..7e125cac11 100644 --- a/components/remote-controls/grid/api/build.gradle.kts +++ b/components/remote-controls/grid/main/api/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("flipper.android-compose") } -android.namespace = "com.flipperdevices.remotecontrols.grid.api" +android.namespace = "com.flipperdevices.remotecontrols.grid.main.api" dependencies { implementation(projects.components.bridge.dao.api) diff --git a/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/ConfigureGridDecomposeComponent.kt b/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/ConfigureGridDecomposeComponent.kt new file mode 100644 index 0000000000..9d719a8104 --- /dev/null +++ b/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/ConfigureGridDecomposeComponent.kt @@ -0,0 +1,16 @@ +package com.flipperdevices.remotecontrols.api + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam +import com.flipperdevices.ui.decompose.CompositeDecomposeComponent +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter + +abstract class ConfigureGridDecomposeComponent : CompositeDecomposeComponent() { + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + param: ServerRemoteControlParam, + onBack: DecomposeOnBackParameter, + ): ConfigureGridDecomposeComponent<*> + } +} diff --git a/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/model/ServerRemoteControlParam.kt b/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/model/ServerRemoteControlParam.kt new file mode 100644 index 0000000000..ca1b3c8804 --- /dev/null +++ b/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/model/ServerRemoteControlParam.kt @@ -0,0 +1,19 @@ +package com.flipperdevices.remotecontrols.api.model + +import com.flipperdevices.bridge.dao.api.model.FlipperKeyType +import com.flipperdevices.bridge.dao.api.model.UI_INFRARED_EXTENSION + +data class ServerRemoteControlParam(val infraredFileId: Long) { + + val key: String + get() = this.toString() + + val extTempFolderPath: String + get() = "${FlipperKeyType.INFRARED.flipperDir}/temp/" + + val nameWithExtension: String + get() = "$infraredFileId.ir" + + val uiFileNameWithExtension: String + get() = "$infraredFileId.$UI_INFRARED_EXTENSION" +} diff --git a/components/remote-controls/grid/main/impl/build.gradle.kts b/components/remote-controls/grid/main/impl/build.gradle.kts new file mode 100644 index 0000000000..f8234c1db7 --- /dev/null +++ b/components/remote-controls/grid/main/impl/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("flipper.android-compose") + id("flipper.anvil") + id("kotlinx-serialization") +} +android.namespace = "com.flipperdevices.remotecontrols.grid.main.impl" + +dependencies { + implementation(projects.components.core.di) + implementation(projects.components.core.ktx) + implementation(projects.components.core.log) + + implementation(projects.components.core.ui.decompose) + + implementation(projects.components.bridge.dao.api) + + implementation(projects.components.remoteControls.grid.main.api) + implementation(projects.components.remoteControls.grid.createControl.api) + implementation(projects.components.remoteControls.grid.remote.api) + implementation(projects.components.remoteControls.grid.saved.api) + implementation(projects.components.keyedit.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.foundation) + + implementation(libs.bundles.decompose) +} diff --git a/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/ConfigureGridDecomposeComponentImpl.kt b/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/ConfigureGridDecomposeComponentImpl.kt new file mode 100644 index 0000000000..ac9cff33ca --- /dev/null +++ b/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/ConfigureGridDecomposeComponentImpl.kt @@ -0,0 +1,82 @@ +package com.flipperdevices.remotecontrols.impl.grid.main + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.pushNew +import com.arkivanov.decompose.router.stack.replaceCurrent +import com.arkivanov.decompose.value.Value +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.keyedit.api.KeyEditDecomposeComponent +import com.flipperdevices.remotecontrols.api.ConfigureGridDecomposeComponent +import com.flipperdevices.remotecontrols.api.CreateControlDecomposeComponent +import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam +import com.flipperdevices.remotecontrols.grid.remote.api.RemoteGridScreenDecomposeComponent +import com.flipperdevices.remotecontrols.impl.grid.main.model.GridNavigationConfig +import com.flipperdevices.ui.decompose.DecomposeComponent +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.popOr +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import me.gulya.anvil.assisted.ContributesAssistedFactory + +@Suppress("LongParameterList") +@ContributesAssistedFactory(AppGraph::class, ConfigureGridDecomposeComponent.Factory::class) +class ConfigureGridDecomposeComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted param: ServerRemoteControlParam, + @Assisted private val onBack: DecomposeOnBackParameter, + private val editorKeyFactory: KeyEditDecomposeComponent.Factory, + private val remoteGridComponentFactory: RemoteGridScreenDecomposeComponent.Factory, + private val createControlComponentFactory: CreateControlDecomposeComponent.Factory, +) : ConfigureGridDecomposeComponent(), ComponentContext by componentContext { + override val stack: Value> = childStack( + source = navigation, + serializer = GridNavigationConfig.serializer(), + initialConfiguration = GridNavigationConfig.ServerControl(param.infraredFileId), + handleBackButton = true, + childFactory = ::child, + ) + + private fun child( + config: GridNavigationConfig, + componentContext: ComponentContext + ): DecomposeComponent = when (config) { + is GridNavigationConfig.Rename -> editorKeyFactory.invoke( + componentContext = componentContext, + onBack = { navigation.popOr(onBack::invoke) }, + onSave = { savedKey -> + if (savedKey == null) { + navigation.popOr(onBack::invoke) + return@invoke + } + navigation.pop() + navigation.replaceCurrent( + GridNavigationConfig.Configuring( + savedKey.getKeyPath(), + config.notSavedFlipperKey + ) + ) + }, + notSavedFlipperKey = config.notSavedFlipperKey, + title = null + ) + + is GridNavigationConfig.ServerControl -> remoteGridComponentFactory.invoke( + componentContext = componentContext, + param = ServerRemoteControlParam(config.id), + onBack = { navigation.popOr(onBack::invoke) }, + onSaveKey = { + navigation.pushNew(GridNavigationConfig.Rename(it)) + } + ) + + is GridNavigationConfig.Configuring -> createControlComponentFactory.invoke( + componentContext = componentContext, + savedKey = config.keyPath, + originalKey = config.notSavedFlipperKey, + onBack = onBack + ) + } +} diff --git a/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/model/GridNavigationConfig.kt b/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/model/GridNavigationConfig.kt new file mode 100644 index 0000000000..f1b8f12182 --- /dev/null +++ b/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/model/GridNavigationConfig.kt @@ -0,0 +1,20 @@ +package com.flipperdevices.remotecontrols.impl.grid.main.model + +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import kotlinx.serialization.Serializable + +@Serializable +sealed class GridNavigationConfig { + @Serializable + data class Rename(val notSavedFlipperKey: NotSavedFlipperKey) : GridNavigationConfig() + + @Serializable + data class ServerControl(val id: Long) : GridNavigationConfig() + + @Serializable + data class Configuring( + val keyPath: FlipperKeyPath, + val notSavedFlipperKey: NotSavedFlipperKey + ) : GridNavigationConfig() +} diff --git a/components/remote-controls/grid/impl/src/main/res/values/strings.xml b/components/remote-controls/grid/main/impl/src/main/res/values/strings.xml similarity index 100% rename from components/remote-controls/grid/impl/src/main/res/values/strings.xml rename to components/remote-controls/grid/main/impl/src/main/res/values/strings.xml diff --git a/components/remote-controls/grid/remote/api/build.gradle.kts b/components/remote-controls/grid/remote/api/build.gradle.kts new file mode 100644 index 0000000000..830cf87238 --- /dev/null +++ b/components/remote-controls/grid/remote/api/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("flipper.android-compose") +} + +android.namespace = "com.flipperdevices.remotecontrols.grid.remote.api" + +dependencies { + implementation(projects.components.core.ui.decompose) + implementation(projects.components.remoteControls.grid.main.api) + implementation(projects.components.keyedit.api) + + implementation(libs.compose.ui) + implementation(libs.decompose) +} diff --git a/components/remote-controls/grid/remote/api/src/main/kotlin/com/flipperdevices/remotecontrols/grid/remote/api/RemoteGridScreenDecomposeComponent.kt b/components/remote-controls/grid/remote/api/src/main/kotlin/com/flipperdevices/remotecontrols/grid/remote/api/RemoteGridScreenDecomposeComponent.kt new file mode 100644 index 0000000000..b2f2ae635b --- /dev/null +++ b/components/remote-controls/grid/remote/api/src/main/kotlin/com/flipperdevices/remotecontrols/grid/remote/api/RemoteGridScreenDecomposeComponent.kt @@ -0,0 +1,20 @@ +package com.flipperdevices.remotecontrols.grid.remote.api + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent + +abstract class RemoteGridScreenDecomposeComponent( + componentContext: ComponentContext +) : ScreenDecomposeComponent(componentContext) { + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + param: ServerRemoteControlParam, + onBack: DecomposeOnBackParameter, + onSaveKey: (NotSavedFlipperKey) -> Unit + ): RemoteGridScreenDecomposeComponent + } +} diff --git a/components/remote-controls/grid/impl/build.gradle.kts b/components/remote-controls/grid/remote/impl/build.gradle.kts similarity index 84% rename from components/remote-controls/grid/impl/build.gradle.kts rename to components/remote-controls/grid/remote/impl/build.gradle.kts index 610babfc5d..41968581be 100644 --- a/components/remote-controls/grid/impl/build.gradle.kts +++ b/components/remote-controls/grid/remote/impl/build.gradle.kts @@ -3,7 +3,7 @@ plugins { id("flipper.anvil") id("kotlinx-serialization") } -android.namespace = "com.flipperdevices.remotecontrols.grid.impl" +android.namespace = "com.flipperdevices.remotecontrols.grid.remote.impl" dependencies { implementation(projects.components.core.di) @@ -18,15 +18,15 @@ dependencies { implementation(projects.components.core.ui.dialog) implementation(projects.components.bridge.dao.api) - implementation(projects.components.bridge.service.api) - implementation(projects.components.bridge.api) implementation(projects.components.infrared.utils) implementation(projects.components.remoteControls.apiBackend) implementation(projects.components.remoteControls.coreModel) implementation(projects.components.remoteControls.coreUi) - implementation(projects.components.remoteControls.grid.api) + implementation(projects.components.remoteControls.grid.main.api) + implementation(projects.components.remoteControls.grid.remote.api) implementation(projects.components.remoteControls.setup.api) + implementation(projects.components.keyedit.api) implementation(projects.components.rootscreen.api) @@ -42,7 +42,5 @@ dependencies { implementation(libs.kotlin.serialization.json) - implementation(libs.decompose) - implementation(libs.bundles.decompose) } diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/RemoteGridComposable.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/RemoteGridComposable.kt new file mode 100644 index 0000000000..362e13dbae --- /dev/null +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/RemoteGridComposable.kt @@ -0,0 +1,68 @@ +package com.flipperdevices.remotecontrols.impl.grid.remote.composable + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar +import com.flipperdevices.remotecontrols.grid.remote.impl.R +import com.flipperdevices.remotecontrols.impl.grid.remote.composable.components.RemoteGridComposableContent +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent + +@Composable +fun RemoteGridComposable( + remoteGridComponent: RemoteGridComponent, + modifier: Modifier = Modifier +) { + val coroutineScope = rememberCoroutineScope() + val scaffoldState = rememberScaffoldState() + val model by remember(remoteGridComponent, coroutineScope) { + remoteGridComponent.model(coroutineScope) + }.collectAsState() + Scaffold( + modifier = modifier, + topBar = { + SharedTopBar( + onBackClick = remoteGridComponent::pop, + actions = { + AnimatedVisibility(model.isFilesSaved) { + Row(modifier = Modifier) { + Text( + text = stringResource(R.string.save), + color = LocalPalletV2.current.text.title.blackOnColor, + style = LocalTypography.current.titleEB18, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.clickableRipple(onClick = remoteGridComponent::save) + ) + } + } + } + ) + }, + backgroundColor = LocalPalletV2.current.surface.backgroundMain.body, + scaffoldState = scaffoldState, + content = { scaffoldPaddings -> + RemoteGridComposableContent( + remoteGridComponent = remoteGridComponent, + model = model, + modifier = Modifier.padding(scaffoldPaddings) + ) + } + ) +} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableContent.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridComposableContent.kt similarity index 59% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableContent.kt rename to components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridComposableContent.kt index 709e9ff3e6..edb4c454ac 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableContent.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridComposableContent.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.composable.components +package com.flipperdevices.remotecontrols.impl.grid.remote.composable.components import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn @@ -9,17 +9,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.flipperdevices.core.ui.dialog.composable.busy.ComposableFlipperBusy import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable +import com.flipperdevices.ifrmvp.core.ui.layout.shared.GridPagesContent import com.flipperdevices.ifrmvp.core.ui.layout.shared.LoadingComposable -import com.flipperdevices.remotecontrols.grid.impl.R -import com.flipperdevices.remotecontrols.impl.grid.composable.util.contentKey -import com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.GridComponent +import com.flipperdevices.remotecontrols.grid.remote.impl.R +import com.flipperdevices.remotecontrols.impl.grid.remote.composable.util.contentKey +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent import com.flipperdevices.rootscreen.api.LocalRootNavigation import com.flipperdevices.rootscreen.model.RootScreenConfig @Composable -internal fun GridComposableContent( - gridComponent: GridComponent, - model: GridComponent.Model, +internal fun RemoteGridComposableContent( + remoteGridComponent: RemoteGridComponent, + model: RemoteGridComponent.Model, modifier: Modifier = Modifier ) { val rootNavigation = LocalRootNavigation.current @@ -27,37 +28,38 @@ internal fun GridComposableContent( targetState = model, modifier = modifier, transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - contentKey = { it.contentKey } + contentKey = { model.contentKey } ) { animatedModel -> when (animatedModel) { - GridComponent.Model.Error -> { + RemoteGridComponent.Model.Error -> { ErrorComposable( desc = stringResource(R.string.empty_page), - onReload = gridComponent::tryLoad + onReload = remoteGridComponent::tryLoad ) } - is GridComponent.Model.Loaded -> { + is RemoteGridComponent.Model.Loaded -> { if (animatedModel.isFlipperBusy) { ComposableFlipperBusy( - onDismiss = gridComponent::dismissBusyDialog, + onDismiss = remoteGridComponent::dismissBusyDialog, goToRemote = { - gridComponent.dismissBusyDialog() + remoteGridComponent.dismissBusyDialog() rootNavigation.push(RootScreenConfig.ScreenStreaming) } ) } - GridComposableLoadedContent( + GridPagesContent( pagesLayout = animatedModel.pagesLayout, onButtonClick = { _, keyIdentifier -> - gridComponent.onButtonClick(keyIdentifier) + remoteGridComponent.onButtonClick(keyIdentifier) }, - onReload = gridComponent::tryLoad, - emulatedKeyIdentifier = animatedModel.emulatedKey + onReload = remoteGridComponent::tryLoad, + emulatedKeyIdentifier = animatedModel.emulatedKey, + isSyncing = animatedModel.isSavingFiles ) } - is GridComponent.Model.Loading -> { + is RemoteGridComponent.Model.Loading -> { LoadingComposable(progress = animatedModel.progress) } } diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/util/GridComponentModelExt.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/util/GridComponentModelExt.kt new file mode 100644 index 0000000000..139d9b38fa --- /dev/null +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/util/GridComponentModelExt.kt @@ -0,0 +1,10 @@ +package com.flipperdevices.remotecontrols.impl.grid.remote.composable.util + +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent + +internal val RemoteGridComponent.Model.contentKey: Any + get() = when (this) { + RemoteGridComponent.Model.Error -> 0 + is RemoteGridComponent.Model.Loaded -> 1 + is RemoteGridComponent.Model.Loading -> 2 + } diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/BackendPagesRepository.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/data/pages/BackendPagesRepository.kt similarity index 91% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/BackendPagesRepository.kt rename to components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/data/pages/BackendPagesRepository.kt index cb162ce4a2..66e1df5e8e 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/BackendPagesRepository.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/data/pages/BackendPagesRepository.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.data.pages +package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.data.pages import com.flipperdevices.core.di.AppGraph import com.flipperdevices.ifrmvp.api.infrared.InfraredBackendApi diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/PagesRepository.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/data/pages/PagesRepository.kt similarity index 73% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/PagesRepository.kt rename to components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/data/pages/PagesRepository.kt index 7e2573bbfc..e6b79c1cf1 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/PagesRepository.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/data/pages/PagesRepository.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.data.pages +package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.data.pages import com.flipperdevices.ifrmvp.model.PagesLayout diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/GridComponent.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/RemoteGridComponent.kt similarity index 62% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/GridComponent.kt rename to components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/RemoteGridComponent.kt index 7a9d3d6061..33d6010aec 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/GridComponent.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/RemoteGridComponent.kt @@ -1,15 +1,17 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.decompose +package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose import com.arkivanov.decompose.ComponentContext import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.ifrmvp.model.PagesLayout import com.flipperdevices.infrared.editor.core.model.InfraredRemote -import com.flipperdevices.remotecontrols.api.GridScreenDecomposeComponent +import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -interface GridComponent { +interface RemoteGridComponent { fun model(coroutineScope: CoroutineScope): StateFlow fun onButtonClick(identifier: IfrKeyIdentifier) @@ -19,6 +21,8 @@ interface GridComponent { fun dismissBusyDialog() + fun save() + sealed interface Model { data class Loading( val progress: Float = 0f, @@ -29,16 +33,21 @@ interface GridComponent { val remotes: ImmutableList, val isFlipperBusy: Boolean = false, val emulatedKey: IfrKeyIdentifier? = null, + val isSavingFiles: Boolean, ) : Model data object Error : Model + + val isFilesSaved: Boolean + get() = this is Loaded && !this.isSavingFiles } fun interface Factory { fun invoke( componentContext: ComponentContext, - param: GridScreenDecomposeComponent.Param, - onPopClick: () -> Unit - ): GridComponent + param: ServerRemoteControlParam, + onBack: DecomposeOnBackParameter, + onSaveKey: (NotSavedFlipperKey) -> Unit + ): RemoteGridComponent } } diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridComponentImpl.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridComponentImpl.kt new file mode 100644 index 0000000000..2f3edcbd34 --- /dev/null +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridComponentImpl.kt @@ -0,0 +1,136 @@ +package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.internal + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.bridge.dao.api.model.FlipperFilePath +import com.flipperdevices.bridge.dao.api.model.FlipperKeyContent +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier +import com.flipperdevices.keyedit.api.NotSavedFlipperFile +import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import com.flipperdevices.remotecontrols.api.DispatchSignalApi +import com.flipperdevices.remotecontrols.api.SaveTempSignalApi +import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.mapping.GridComponentStateMapper +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.viewmodel.RemoteGridViewModel +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import me.gulya.anvil.assisted.ContributesAssistedFactory +import javax.inject.Provider + +@Suppress("LongParameterList") +@ContributesAssistedFactory(AppGraph::class, RemoteGridComponent.Factory::class) +class RemoteGridComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted private val param: ServerRemoteControlParam, + @Assisted private val onBack: DecomposeOnBackParameter, + @Assisted private val onSaveKey: (NotSavedFlipperKey) -> Unit, + createRemoteGridViewModel: RemoteGridViewModel.Factory, + createSaveTempSignalApi: Provider, + createDispatchSignalApi: Provider, +) : RemoteGridComponent, ComponentContext by componentContext { + private val saveTempSignalApi = instanceKeeper.getOrCreate( + key = "GridComponent_saveSignalViewModel_${param.key}", + factory = { + createSaveTempSignalApi.get() + } + ) + private val dispatchSignalApi = instanceKeeper.getOrCreate( + key = "GridComponent_dispatchSignalViewModel_${param.key}", + factory = { + createDispatchSignalApi.get() + } + ) + private val remoteGridViewModel = instanceKeeper.getOrCreate( + key = "GridComponent_gridFeature_${param.key}", + factory = { + createRemoteGridViewModel.invoke( + param = param, + onCallback = { callback -> + when (callback) { + is RemoteGridViewModel.Callback.ContentLoaded -> { + saveTempSignalApi.saveFiles( + SaveTempSignalApi.FileDesc( + textContent = callback.infraredContent, + nameWithExtension = param.nameWithExtension, + extFolderPath = param.extTempFolderPath + ), + SaveTempSignalApi.FileDesc( + textContent = callback.uiContent, + nameWithExtension = param.uiFileNameWithExtension, + extFolderPath = param.extTempFolderPath + ) + ) + } + } + } + ) + } + ) + + override fun model(coroutineScope: CoroutineScope) = combine( + saveTempSignalApi.state, + remoteGridViewModel.state, + dispatchSignalApi.state, + transform = { saveState, gridState, dispatchState -> + GridComponentStateMapper.map( + saveState = saveState, + gridState = gridState, + dispatchState = dispatchState, + ) + } + ).stateIn(coroutineScope, SharingStarted.Eagerly, RemoteGridComponent.Model.Loading()) + + override fun dismissBusyDialog() { + dispatchSignalApi.dismissBusyDialog() + } + + override fun save() { + val rawRemotes = remoteGridViewModel.getRawRemotesContent() ?: return + val rawUi = remoteGridViewModel.getRawPagesContent() ?: return + + val notSavedFlipperFile = NotSavedFlipperKey( + mainFile = NotSavedFlipperFile( + FlipperFilePath( + folder = param.extTempFolderPath, + nameWithExtension = param.nameWithExtension + ), + content = FlipperKeyContent.RawData(rawRemotes.toByteArray()) + ), + additionalFiles = listOf( + NotSavedFlipperFile( + path = FlipperFilePath( + folder = param.extTempFolderPath, + nameWithExtension = param.uiFileNameWithExtension + ), + content = FlipperKeyContent.RawData(rawUi.toByteArray()) + ) + ), + notes = null + ) + onSaveKey.invoke(notSavedFlipperFile) + } + + override fun onButtonClick(identifier: IfrKeyIdentifier) { + val gridLoadedState = + (remoteGridViewModel.state.value as? RemoteGridViewModel.State.Loaded) ?: return + val remotes = gridLoadedState.remotes + dispatchSignalApi.dispatch( + identifier = identifier, + remotes = remotes, + ffPath = FlipperFilePath( + folder = param.extTempFolderPath, + nameWithExtension = param.nameWithExtension + ) + ) + } + + override fun tryLoad() = remoteGridViewModel.tryLoad() + override fun pop() = onBack.invoke() +} diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridScreenDecomposeComponentImpl.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridScreenDecomposeComponentImpl.kt new file mode 100644 index 0000000000..5d5848671f --- /dev/null +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridScreenDecomposeComponentImpl.kt @@ -0,0 +1,36 @@ +package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.internal + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.childContext +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam +import com.flipperdevices.remotecontrols.grid.remote.api.RemoteGridScreenDecomposeComponent +import com.flipperdevices.remotecontrols.impl.grid.remote.composable.RemoteGridComposable +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import me.gulya.anvil.assisted.ContributesAssistedFactory + +@ContributesAssistedFactory(AppGraph::class, RemoteGridScreenDecomposeComponent.Factory::class) +class RemoteGridScreenDecomposeComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted param: ServerRemoteControlParam, + @Assisted onBack: DecomposeOnBackParameter, + @Assisted onSaveKey: (NotSavedFlipperKey) -> Unit, + remoteGridComponentFactory: RemoteGridComponent.Factory +) : RemoteGridScreenDecomposeComponent(componentContext) { + private val gridComponent = remoteGridComponentFactory.invoke( + componentContext = childContext("GridComponent"), + param = param, + onBack = onBack, + onSaveKey = onSaveKey + ) + + @Composable + override fun Render() { + RemoteGridComposable(gridComponent) + } +} diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/mapping/GridComponentStateMapper.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/mapping/GridComponentStateMapper.kt new file mode 100644 index 0000000000..4dd3cea1da --- /dev/null +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/mapping/GridComponentStateMapper.kt @@ -0,0 +1,34 @@ +package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.mapping + +import com.flipperdevices.remotecontrols.api.DispatchSignalApi +import com.flipperdevices.remotecontrols.api.SaveTempSignalApi +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.viewmodel.RemoteGridViewModel + +internal object GridComponentStateMapper { + fun map( + saveState: SaveTempSignalApi.State, + gridState: RemoteGridViewModel.State, + dispatchState: DispatchSignalApi.State, + ): RemoteGridComponent.Model = when (gridState) { + RemoteGridViewModel.State.Error -> RemoteGridComponent.Model.Error + is RemoteGridViewModel.State.Loaded -> { + when (saveState) { + SaveTempSignalApi.State.Error -> RemoteGridComponent.Model.Error + is SaveTempSignalApi.State.Uploading, + SaveTempSignalApi.State.Uploaded, + SaveTempSignalApi.State.Pending -> { + RemoteGridComponent.Model.Loaded( + pagesLayout = gridState.pagesLayout, + remotes = gridState.remotes, + isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, + emulatedKey = (dispatchState as? DispatchSignalApi.State.Emulating)?.ifrKeyIdentifier, + isSavingFiles = saveState is SaveTempSignalApi.State.Uploading, + ) + } + } + } + + RemoteGridViewModel.State.Loading -> RemoteGridComponent.Model.Loading(0f) + } +} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/viewmodel/GridViewModel.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/viewmodel/RemoteGridViewModel.kt similarity index 51% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/viewmodel/GridViewModel.kt rename to components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/viewmodel/RemoteGridViewModel.kt index 44a2b3b278..ba56d56bde 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/viewmodel/GridViewModel.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/viewmodel/RemoteGridViewModel.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.viewmodel +package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.viewmodel import com.flipperdevices.bridge.dao.api.model.FlipperFileFormat import com.flipperdevices.core.log.LogTagProvider @@ -7,11 +7,8 @@ import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel import com.flipperdevices.ifrmvp.model.PagesLayout import com.flipperdevices.infrared.editor.core.model.InfraredRemote import com.flipperdevices.infrared.editor.core.parser.InfraredKeyParser -import com.flipperdevices.remotecontrols.api.GridScreenDecomposeComponent -import com.flipperdevices.remotecontrols.impl.grid.presentation.data.localpages.LocalPagesRepository -import com.flipperdevices.remotecontrols.impl.grid.presentation.data.pages.PagesRepository -import com.flipperdevices.remotecontrols.impl.grid.presentation.util.GridParamExt.flipperFilePath -import com.flipperdevices.remotecontrols.impl.grid.presentation.util.GridParamExt.irFileIdOrNull +import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.data.pages.PagesRepository import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -24,10 +21,9 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @Suppress("LongParameterList") -class GridViewModel @AssistedInject constructor( +class RemoteGridViewModel @AssistedInject constructor( private val pagesRepository: PagesRepository, - private val localPagesRepository: LocalPagesRepository, - @Assisted private val param: GridScreenDecomposeComponent.Param, + @Assisted private val param: ServerRemoteControlParam, @Assisted private val onCallback: (Callback) -> Unit, ) : DecomposeViewModel(), LogTagProvider { override val TAG: String = "GridViewModel" @@ -43,37 +39,24 @@ class GridViewModel @AssistedInject constructor( fun tryLoad() { viewModelScope.launch { - val localPagesLayout = localPagesRepository.getLocalPagesLayout( - path = param.flipperFilePath, - toPagesLayout = { rawContent -> - runCatching { - json.decodeFromString(rawContent) - }.getOrNull() - } - ) - val localRemotesRaw = localPagesRepository.getLocalFlipperKey(param.flipperFilePath) - ?.keyContent - ?.openStream() - ?.reader() - ?.readText() - val pagesLayout = localPagesLayout - ?: pagesRepository.fetchDefaultPageLayout(ifrFileId = param.irFileIdOrNull ?: -1) - .onSuccess { pagesLayout -> - onCallback.invoke(Callback.UiLoaded(json.encodeToString(pagesLayout))) - } + val pagesLayout = + pagesRepository.fetchDefaultPageLayout(ifrFileId = param.infraredFileId) .onFailure { _state.emit(State.Error) } .onFailure { throwable -> error(throwable) { "#tryLoad could not load ui model" } } .getOrNull() ?: return@launch + val pagesLayoutRaw = json.encodeToString(pagesLayout) - val remotesRaw = localRemotesRaw - ?: pagesRepository.fetchKeyContent(param.irFileIdOrNull ?: -1) - .onFailure { _state.emit(State.Error) } - .onFailure { throwable -> error(throwable) { "#tryLoad could not load key content" } } - .onSuccess { - onCallback.invoke(Callback.InfraredFileLoaded(it)) - } - .getOrNull() - .orEmpty() + val remotesRaw = pagesRepository.fetchKeyContent(param.infraredFileId) + .onFailure { _state.emit(State.Error) } + .onFailure { throwable -> error(throwable) { "#tryLoad could not load key content" } } + .getOrNull() + .orEmpty() + + val callback = Callback.ContentLoaded( + infraredContent = remotesRaw, + uiContent = pagesLayoutRaw + ) + onCallback.invoke(callback) _state.emit( value = State.Loaded( pagesLayout = pagesLayout, @@ -82,12 +65,22 @@ class GridViewModel @AssistedInject constructor( .let(InfraredKeyParser::mapParsedKeyToInfraredRemotes) .toImmutableList(), remotesRaw = remotesRaw, - isDownloadedOnFlipper = localPagesLayout != null && localRemotesRaw != null + isDownloadedOnFlipper = true ) ) } } + fun getRawRemotesContent(): String? { + val state = state.value as? State.Loaded ?: return null + return state.remotesRaw + } + + fun getRawPagesContent(): String? { + val state = state.value as? State.Loaded ?: return null + return json.encodeToString(state.pagesLayout) + } + init { tryLoad() } @@ -106,13 +99,15 @@ class GridViewModel @AssistedInject constructor( @AssistedFactory fun interface Factory { operator fun invoke( - param: GridScreenDecomposeComponent.Param, + param: ServerRemoteControlParam, onCallback: (Callback) -> Unit, - ): GridViewModel + ): RemoteGridViewModel } sealed interface Callback { - data class InfraredFileLoaded(val content: String) : Callback - data class UiLoaded(val content: String) : Callback + data class ContentLoaded( + val infraredContent: String, + val uiContent: String + ) : Callback } } diff --git a/components/remote-controls/grid/remote/impl/src/main/res/values/strings.xml b/components/remote-controls/grid/remote/impl/src/main/res/values/strings.xml new file mode 100644 index 0000000000..2d1dc332e1 --- /dev/null +++ b/components/remote-controls/grid/remote/impl/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Page is empty! + Save + \ No newline at end of file diff --git a/components/remote-controls/grid/saved/api/build.gradle.kts b/components/remote-controls/grid/saved/api/build.gradle.kts new file mode 100644 index 0000000000..b8fbfea19f --- /dev/null +++ b/components/remote-controls/grid/saved/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("flipper.android-compose") +} + +android.namespace = "com.flipperdevices.remotecontrols.grid.saved.api" + +dependencies { + implementation(projects.components.bridge.dao.api) + + implementation(projects.components.core.ui.decompose) + implementation(projects.components.remoteControls.grid.main.api) + + implementation(libs.compose.ui) + implementation(libs.decompose) +} diff --git a/components/remote-controls/grid/saved/api/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/api/LocalGridScreenDecomposeComponent.kt b/components/remote-controls/grid/saved/api/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/api/LocalGridScreenDecomposeComponent.kt new file mode 100644 index 0000000000..e8dbce23df --- /dev/null +++ b/components/remote-controls/grid/saved/api/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/api/LocalGridScreenDecomposeComponent.kt @@ -0,0 +1,26 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.api + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent + +abstract class LocalGridScreenDecomposeComponent( + componentContext: ComponentContext +) : ScreenDecomposeComponent(componentContext) { + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + keyPath: FlipperKeyPath, + onBack: DecomposeOnBackParameter, + onCallback: (Callback) -> Unit + ): LocalGridScreenDecomposeComponent + } + + sealed interface Callback { + data object UiFileNotFound : Callback + data object Deleted : Callback + data class Rename(val keyPath: FlipperKeyPath) : Callback + data class ViewRemoteInfo(val keyPath: FlipperKeyPath) : Callback + } +} diff --git a/components/remote-controls/grid/saved/impl/build.gradle.kts b/components/remote-controls/grid/saved/impl/build.gradle.kts new file mode 100644 index 0000000000..5f03351a0c --- /dev/null +++ b/components/remote-controls/grid/saved/impl/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("flipper.android-compose") + id("flipper.anvil") + id("kotlinx-serialization") +} +android.namespace = "com.flipperdevices.remotecontrols.grid.saved.impl" + +dependencies { + implementation(projects.components.core.di) + implementation(projects.components.core.ktx) + implementation(projects.components.core.log) + + implementation(projects.components.core.ui.lifecycle) + implementation(projects.components.core.ui.theme) + implementation(projects.components.core.ui.decompose) + implementation(projects.components.core.ui.ktx) + implementation(projects.components.core.ui.res) + implementation(projects.components.core.ui.dialog) + + implementation(projects.components.bridge.dao.api) + implementation(projects.components.bridge.synchronization.api) + implementation(projects.components.infrared.utils) + + implementation(projects.components.remoteControls.coreModel) + implementation(projects.components.remoteControls.coreUi) + implementation(projects.components.remoteControls.grid.main.api) + implementation(projects.components.remoteControls.grid.saved.api) + implementation(projects.components.remoteControls.setup.api) + + implementation(projects.components.rootscreen.api) + + implementation(projects.components.keyscreen.api) + implementation(projects.components.share.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.tooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + implementation(libs.compose.material.icons.core) + implementation(libs.compose.material.icons.extended) + + implementation(libs.kotlin.immutable.collections) + + implementation(libs.kotlin.serialization.json) + + implementation(libs.bundles.decompose) +} diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/LocalGridComposable.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/LocalGridComposable.kt new file mode 100644 index 0000000000..3e812ba98d --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/LocalGridComposable.kt @@ -0,0 +1,96 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Scaffold +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flipperdevices.bridge.synchronization.api.SynchronizationState +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar +import com.flipperdevices.remotecontrols.impl.grid.local.api.LocalGridScreenDecomposeComponent +import com.flipperdevices.remotecontrols.impl.grid.local.composable.components.ComposableInfraredDropDown +import com.flipperdevices.remotecontrols.impl.grid.local.composable.components.ComposableSynchronizationNotification +import com.flipperdevices.remotecontrols.impl.grid.local.composable.components.LocalGridComposableContent +import com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.LocalGridComponent + +@Composable +@Suppress("LongMethod") +fun LocalGridComposable( + localGridComponent: LocalGridComponent, + onCallback: (LocalGridScreenDecomposeComponent.Callback) -> Unit, + onShare: () -> Unit, + modifier: Modifier = Modifier +) { + val coroutineScope = rememberCoroutineScope() + val scaffoldState = rememberScaffoldState() + val model by remember(localGridComponent, coroutineScope) { + localGridComponent.model(coroutineScope) + }.collectAsState() + LaunchedEffect(model) { + if (model is LocalGridComponent.Model.Error) { + onCallback.invoke(LocalGridScreenDecomposeComponent.Callback.UiFileNotFound) + } + } + Scaffold( + modifier = modifier, + topBar = { + (model as? LocalGridComponent.Model.Loaded)?.let { loadedModel -> + SharedTopBar(onBackClick = localGridComponent::pop) { + ComposableInfraredDropDown( + onRename = { + localGridComponent.onRename { + onCallback.invoke( + LocalGridScreenDecomposeComponent.Callback.Rename(loadedModel.keyPath) + ) + } + }, + onDelete = { + localGridComponent.onDelete { + onCallback.invoke(LocalGridScreenDecomposeComponent.Callback.Deleted) + } + }, + onRemoteInfo = { + onCallback.invoke( + LocalGridScreenDecomposeComponent.Callback.ViewRemoteInfo(loadedModel.keyPath) + ) + }, + onShare = onShare, + emulatingInProgress = loadedModel.emulatedKey != null + ) + } + } + }, + backgroundColor = LocalPalletV2.current.surface.backgroundMain.body, + scaffoldState = scaffoldState, + content = { scaffoldPaddings -> + LocalGridComposableContent( + localGridComponent = localGridComponent, + model = model, + modifier = Modifier.padding(scaffoldPaddings) + ) + Box( + modifier = Modifier + .fillMaxSize() + .navigationBarsPadding() + .padding(14.dp), + contentAlignment = Alignment.BottomCenter + ) { + val state = (model as? LocalGridComponent.Model.Loaded) + ?.synchronizationState + ?: SynchronizationState.NotStarted + ComposableSynchronizationNotification(state) + } + } + ) +} diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/ComposableSyncNotification.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/ComposableSyncNotification.kt new file mode 100644 index 0000000000..627be851e2 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/ComposableSyncNotification.kt @@ -0,0 +1,172 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.composable.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.SnackbarDuration +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +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.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flipperdevices.bridge.synchronization.api.SynchronizationState +import com.flipperdevices.core.ui.theme.FlipperThemeInternal +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.remotecontrols.grid.saved.impl.R +import kotlinx.coroutines.delay + +@Composable +internal fun ComposableNotification( + icon: @Composable RowScope.() -> Unit, + text: String, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(LocalPalletV2.current.surface.popUp.body.default) + .padding(14.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + icon.invoke(this) + Text( + modifier = Modifier + .padding(top = 9.dp, bottom = 9.dp, end = 12.dp) + .weight(1f), + text = text, + style = LocalTypography.current.subtitleB12 + ) + } +} + +@Composable +fun ComposableSyncingNotification(modifier: Modifier = Modifier) { + ComposableNotification( + modifier = modifier, + icon = { + val transition = rememberInfiniteTransition() + val angle by transition.animateFloat( + label = "Icon rotation", + initialValue = 0F, + targetValue = 360F, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 2000, + easing = LinearEasing + ) + ), + ) + Icon( + painter = painterResource(R.drawable.ic_syncing), + contentDescription = null, + modifier = Modifier + .size(24.dp) + .rotate(angle), + tint = LocalPalletV2.current.text.link.default + ) + }, + text = stringResource(R.string.sync_state_syncing) + ) +} + +@Composable +fun ComposableSyncedNotification(modifier: Modifier = Modifier) { + ComposableNotification( + modifier = modifier, + icon = { + Icon( + painter = painterResource(R.drawable.ic_synced), + contentDescription = null, + modifier = Modifier + .size(24.dp), + tint = LocalPalletV2.current.text.link.default + ) + }, + text = stringResource(R.string.sync_state_synced) + ) +} + +@Composable +fun ComposableNotConnectedNotification(modifier: Modifier = Modifier) { + ComposableNotification( + modifier = modifier, + icon = { + Icon( + painter = painterResource(R.drawable.ic_not_connected), + contentDescription = null, + modifier = Modifier + .size(24.dp), + tint = LocalPalletV2.current.illustration.blackAndWhite.black + ) + }, + text = stringResource(R.string.sync_state_not_connected) + ) +} + +private const val VISIBILITY_DURATION = 2000L + +@Composable +fun ComposableSynchronizationNotification(state: SynchronizationState) { + var isVisible by remember(Unit) { mutableStateOf(false) } + LaunchedEffect(state, isVisible) { + if (state is SynchronizationState.NotStarted) isVisible = false + if (state is SynchronizationState.InProgress) isVisible = true + if (state is SynchronizationState.Finished) { + SnackbarDuration.Short + delay(VISIBILITY_DURATION) + isVisible = false + } + } + AnimatedVisibility( + visible = isVisible, + enter = fadeIn(), + exit = fadeOut(), + content = { + when (state) { + SynchronizationState.Finished -> ComposableSyncedNotification() + is SynchronizationState.InProgress -> ComposableSyncingNotification() + SynchronizationState.NotStarted -> Unit + } + } + ) +} + +@Preview +@Composable +private fun ComposableNotificationPreview() { + FlipperThemeInternal { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + ComposableSyncingNotification() + ComposableSyncedNotification() + ComposableNotConnectedNotification() + } + } +} diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/GridOptions.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/GridOptions.kt new file mode 100644 index 0000000000..fd1ba2551b --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/GridOptions.kt @@ -0,0 +1,152 @@ +@file:Suppress("CompositionLocalAllowlist") + +package com.flipperdevices.remotecontrols.impl.grid.local.composable.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.clickableRipple +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.saved.impl.R +import com.flipperdevices.core.ui.res.R as SharedRes + +@Composable +@Suppress("LongMethod") +internal fun ComposableInfraredDropDown( + onRemoteInfo: () -> Unit, + onRename: () -> Unit, + onShare: () -> Unit, + onDelete: () -> Unit, + emulatingInProgress: Boolean, + modifier: Modifier = Modifier, +) { + var isShowMoreOptions by remember { mutableStateOf(false) } + val onChangeState = { isShowMoreOptions = !isShowMoreOptions } + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.End + ) { + Icon( + modifier = Modifier + .clickableRipple(bounded = false, onClick = onChangeState) + .size(24.dp), + tint = LocalPalletV2.current.icon.blackAndWhite.blackOnColor, + painter = painterResource(SharedRes.drawable.ic_more_points), + contentDescription = null + ) + DropdownMenu( + expanded = isShowMoreOptions, + onDismissRequest = { isShowMoreOptions = false } + ) { + ComposableInfraredDropDownItem( + text = stringResource(R.string.option_remote_info), + painter = painterResource(R.drawable.ic_how_to_use), + onClick = { + onRemoteInfo.invoke() + onChangeState.invoke() + } + ) + ComposableInfraredDropDownItem( + text = stringResource(R.string.option_rename), + painter = painterResource(R.drawable.ic_edit), + onClick = { + onChangeState.invoke() + onRename.invoke() + }, + isActive = !emulatingInProgress + ) + Divider(modifier = Modifier.padding(horizontal = 8.dp)) + Divider(modifier = Modifier.padding(horizontal = 8.dp)) + ComposableInfraredDropDownItem( + text = stringResource(R.string.option_share), + painter = painterResource(SharedRes.drawable.ic_upload), + onClick = { + onChangeState.invoke() + onShare.invoke() + } + ) + Divider(modifier = Modifier.padding(horizontal = 8.dp)) + ComposableInfraredDropDownItem( + text = stringResource(R.string.option_delete), + painter = painterResource(SharedRes.drawable.ic_trash_icon), + colorText = LocalPallet.current.keyDelete, + colorIcon = LocalPallet.current.keyDelete, + onClick = { + onChangeState.invoke() + onDelete.invoke() + } + ) + } + } +} + +@Composable +private fun ComposableInfraredDropDownItem( + text: String, + painter: Painter, + isActive: Boolean = true, + colorText: Color = LocalPallet.current.text100, + colorIcon: Color = LocalPallet.current.text100, + onClick: () -> Unit, +) { + DropdownMenuItem( + onClick = { + if (isActive) { + onClick() + } + }, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp) + ) { + Column { + Row( + modifier = Modifier + .padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painter, + tint = if (isActive) { + colorIcon + } else { + LocalPallet.current.keyScreenDisabled + }, + contentDescription = null + ) + Text( + text = text, + style = LocalTypography.current.bodyM14, + color = if (isActive) { + colorText + } else { + LocalPallet.current.keyScreenDisabled + } + ) + } + } + } +} diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/LocalGridComposableContent.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/LocalGridComposableContent.kt new file mode 100644 index 0000000000..21821504f0 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/LocalGridComposableContent.kt @@ -0,0 +1,59 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.composable.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.flipperdevices.core.ui.dialog.composable.busy.ComposableFlipperBusy +import com.flipperdevices.ifrmvp.core.ui.layout.shared.GridPagesContent +import com.flipperdevices.ifrmvp.core.ui.layout.shared.LoadingComposable +import com.flipperdevices.remotecontrols.impl.grid.local.composable.util.contentKey +import com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.LocalGridComponent +import com.flipperdevices.rootscreen.api.LocalRootNavigation +import com.flipperdevices.rootscreen.model.RootScreenConfig + +@Composable +internal fun LocalGridComposableContent( + localGridComponent: LocalGridComponent, + model: LocalGridComponent.Model, + modifier: Modifier = Modifier +) { + val rootNavigation = LocalRootNavigation.current + AnimatedContent( + targetState = model, + modifier = modifier, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + contentKey = { model.contentKey } + ) { animatedModel -> + when (animatedModel) { + // We leave screen on error + LocalGridComponent.Model.Error -> Unit + + is LocalGridComponent.Model.Loaded -> { + if (animatedModel.isFlipperBusy) { + ComposableFlipperBusy( + onDismiss = localGridComponent::dismissBusyDialog, + goToRemote = { + localGridComponent.dismissBusyDialog() + rootNavigation.push(RootScreenConfig.ScreenStreaming) + } + ) + } + GridPagesContent( + pagesLayout = animatedModel.pagesLayout, + onButtonClick = { _, keyIdentifier -> + localGridComponent.onButtonClick(keyIdentifier) + }, + emulatedKeyIdentifier = animatedModel.emulatedKey, + isSyncing = animatedModel.isSynchronizing + ) + } + + is LocalGridComponent.Model.Loading -> { + LoadingComposable() + } + } + } +} diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/util/GridComponentModelExt.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/util/GridComponentModelExt.kt new file mode 100644 index 0000000000..e1b525ad25 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/util/GridComponentModelExt.kt @@ -0,0 +1,10 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.composable.util + +import com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.LocalGridComponent + +internal val LocalGridComponent.Model.contentKey: Any + get() = when (this) { + LocalGridComponent.Model.Error -> 0 + is LocalGridComponent.Model.Loaded -> 1 + is LocalGridComponent.Model.Loading -> 2 + } diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/LocalGridComponent.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/LocalGridComponent.kt new file mode 100644 index 0000000000..2f2ae2fdd5 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/LocalGridComponent.kt @@ -0,0 +1,45 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.bridge.synchronization.api.SynchronizationState +import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier +import com.flipperdevices.ifrmvp.model.PagesLayout +import com.flipperdevices.infrared.editor.core.model.InfraredRemote +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +interface LocalGridComponent { + fun model(coroutineScope: CoroutineScope): StateFlow + fun onButtonClick(identifier: IfrKeyIdentifier) + fun onRename(onEndAction: (FlipperKeyPath) -> Unit) + fun onDelete(onEndAction: () -> Unit) + fun pop() + fun dismissBusyDialog() + + sealed interface Model { + data object Loading : Model + data class Loaded( + val pagesLayout: PagesLayout, + val remotes: ImmutableList, + val isFlipperBusy: Boolean, + val emulatedKey: IfrKeyIdentifier?, + val synchronizationState: SynchronizationState, + val keyPath: FlipperKeyPath + ) : Model { + val isSynchronizing = synchronizationState is SynchronizationState.InProgress + } + + data object Error : Model + } + + fun interface Factory { + fun invoke( + componentContext: ComponentContext, + keyPath: FlipperKeyPath, + onBack: DecomposeOnBackParameter, + ): LocalGridComponent + } +} diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridComponentImpl.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridComponentImpl.kt new file mode 100644 index 0000000000..93de10d613 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridComponentImpl.kt @@ -0,0 +1,82 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.internal + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.bridge.synchronization.api.SynchronizationApi +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier +import com.flipperdevices.remotecontrols.api.DispatchSignalApi +import com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.LocalGridComponent +import com.flipperdevices.remotecontrols.impl.grid.local.presentation.viewmodel.LocalGridViewModel +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import me.gulya.anvil.assisted.ContributesAssistedFactory +import javax.inject.Provider + +@ContributesAssistedFactory(AppGraph::class, LocalGridComponent.Factory::class) +class LocalGridComponentImpl @AssistedInject constructor( + @Assisted private val componentContext: ComponentContext, + @Assisted private val keyPath: FlipperKeyPath, + @Assisted private val onBack: DecomposeOnBackParameter, + createLocalGridViewModel: LocalGridViewModel.Factory, + createDispatchSignalApi: Provider, + private val synchronizationApi: SynchronizationApi +) : LocalGridComponent, ComponentContext by componentContext { + private val localGridViewModel = instanceKeeper.getOrCreate( + key = "LocalGridComponent_localGridViewModel_$keyPath", + factory = { createLocalGridViewModel.invoke(keyPath) } + ) + private val dispatchSignalApi = instanceKeeper.getOrCreate( + key = "LocalGridComponent_dispatchSignalApi_$keyPath", + factory = { createDispatchSignalApi.get() } + ) + + override fun model(coroutineScope: CoroutineScope) = combine( + flow = localGridViewModel.state, + flow2 = dispatchSignalApi.state, + flow3 = synchronizationApi.getSynchronizationState(), + transform = { gridState, dispatchState, syncState -> + when (gridState) { + LocalGridViewModel.State.Error -> LocalGridComponent.Model.Error + is LocalGridViewModel.State.Loaded -> LocalGridComponent.Model.Loaded( + pagesLayout = gridState.pagesLayout, + remotes = gridState.remotes, + isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, + emulatedKey = (dispatchState as? DispatchSignalApi.State.Emulating)?.ifrKeyIdentifier, + synchronizationState = syncState, + keyPath = gridState.keyPath + ) + + LocalGridViewModel.State.Loading -> LocalGridComponent.Model.Loading + } + } + ).stateIn(coroutineScope, SharingStarted.Eagerly, LocalGridComponent.Model.Loading) + + override fun onButtonClick(identifier: IfrKeyIdentifier) { + val gridLoadedState = + (localGridViewModel.state.value as? LocalGridViewModel.State.Loaded) ?: return + val remotes = gridLoadedState.remotes + + dispatchSignalApi.dispatch( + identifier = identifier, + remotes = remotes, + ffPath = gridLoadedState.keyPath.path + ) + } + + override fun onRename(onEndAction: (FlipperKeyPath) -> Unit) = localGridViewModel.onRename(onEndAction) + + override fun onDelete(onEndAction: () -> Unit) = localGridViewModel.onDelete(onEndAction) + + override fun pop() = onBack.invoke() + + override fun dismissBusyDialog() { + dispatchSignalApi.dismissBusyDialog() + } +} diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridScreenDecomposeComponentImpl.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridScreenDecomposeComponentImpl.kt new file mode 100644 index 0000000000..31a2f3f236 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridScreenDecomposeComponentImpl.kt @@ -0,0 +1,68 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.internal + +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.arkivanov.decompose.childContext +import com.arkivanov.essenty.backhandler.BackCallback +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.remotecontrols.impl.grid.local.api.LocalGridScreenDecomposeComponent +import com.flipperdevices.remotecontrols.impl.grid.local.composable.LocalGridComposable +import com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.LocalGridComponent +import com.flipperdevices.share.api.ShareBottomUIApi +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import me.gulya.anvil.assisted.ContributesAssistedFactory + +@ContributesAssistedFactory(AppGraph::class, LocalGridScreenDecomposeComponent.Factory::class) +class LocalGridScreenDecomposeComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted private val keyPath: FlipperKeyPath, + @Assisted onBack: DecomposeOnBackParameter, + @Assisted private val onCallback: (Callback) -> Unit, + localGridComponentFactory: LocalGridComponent.Factory, + private val shareBottomUiApi: ShareBottomUIApi, +) : LocalGridScreenDecomposeComponent(componentContext) { + private val localGridComponent = localGridComponentFactory.invoke( + componentContext = childContext("GridComponent_local"), + keyPath = keyPath, + onBack = onBack, + ) + private val isBackPressHandledFlow = MutableStateFlow(false) + private val backCallback = BackCallback(false) { isBackPressHandledFlow.update { true } } + + init { + backHandler.register(backCallback) + } + + @Composable + override fun Render() { + shareBottomUiApi.ComposableShareBottomSheet( + keyPath, + onSheetStateVisible = { isShown, onClose -> + val isBackPressHandled by isBackPressHandledFlow.collectAsState() + backCallback.isEnabled = isShown + + LaunchedEffect(isBackPressHandled) { + if (isBackPressHandled) { + onClose() + isBackPressHandledFlow.emit(false) + } + } + }, + componentContext = this + ) { onShare -> + LocalGridComposable( + localGridComponent = localGridComponent, + onCallback = onCallback, + onShare = onShare + ) + } + } +} diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/viewmodel/LocalGridViewModel.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/viewmodel/LocalGridViewModel.kt new file mode 100644 index 0000000000..ec920db255 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/viewmodel/LocalGridViewModel.kt @@ -0,0 +1,88 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.presentation.viewmodel + +import com.flipperdevices.bridge.dao.api.model.FlipperFileFormat +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.ifrmvp.model.PagesLayout +import com.flipperdevices.infrared.editor.core.model.InfraredRemote +import com.flipperdevices.infrared.editor.core.parser.InfraredKeyParser +import com.flipperdevices.keyscreen.api.KeyStateHelperApi +import com.flipperdevices.keyscreen.model.KeyScreenState +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.serialization.json.Json + +class LocalGridViewModel @AssistedInject constructor( + @Assisted private val keyPath: FlipperKeyPath, + keyStateHelperApi: KeyStateHelperApi.Builder, +) : DecomposeViewModel(), LogTagProvider { + override val TAG: String = "LocalGridViewModel" + private val json: Json = Json { + prettyPrint = false + ignoreUnknownKeys = true + isLenient = true + } + + private val keyStateHelper = keyStateHelperApi.build(keyPath, viewModelScope) + + val state = keyStateHelper.getKeyScreenState() + .map { + when (it) { + is KeyScreenState.Error -> State.Error + KeyScreenState.InProgress -> State.Loading + is KeyScreenState.Ready -> { + val keyPath = it.flipperKey.getKeyPath() + val pagesLayout = it.flipperKey.additionalFiles.firstNotNullOfOrNull { fFile -> + val text = fFile.content.openStream().reader().readText() + runCatching { + json.decodeFromString(text) + }.getOrNull() + } + val remotes = it.flipperKey.keyContent + .openStream() + .reader() + .readText() + .let(FlipperFileFormat::fromFileContent) + .let(InfraredKeyParser::mapParsedKeyToInfraredRemotes) + .toImmutableList() + if (pagesLayout == null) { + State.Error + } else { + State.Loaded( + pagesLayout = pagesLayout, + remotes = remotes, + keyPath = keyPath + ) + } + } + } + }.stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + + fun onRename(onEndAction: (FlipperKeyPath) -> Unit) = keyStateHelper.onOpenEdit(onEndAction) + + fun onDelete(onEndAction: () -> Unit) = keyStateHelper.onDelete(onEndAction) + + sealed interface State { + data object Loading : State + data object Error : State + data class Loaded( + val pagesLayout: PagesLayout, + val remotes: ImmutableList, + val keyPath: FlipperKeyPath + ) : State + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + keyPath: FlipperKeyPath, + ): LocalGridViewModel + } +} diff --git a/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_edit.xml b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000000..1897490011 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_how_to_use.xml b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_how_to_use.xml new file mode 100644 index 0000000000..09a3f21c96 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_how_to_use.xml @@ -0,0 +1,14 @@ + + + + diff --git a/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_not_connected.xml b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_not_connected.xml new file mode 100644 index 0000000000..4613b833fb --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_not_connected.xml @@ -0,0 +1,10 @@ + + + diff --git a/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_synced.xml b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_synced.xml new file mode 100644 index 0000000000..ddfa5549d4 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_synced.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_syncing.xml b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_syncing.xml new file mode 100644 index 0000000000..0e276c2ae1 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/res/drawable/ic_syncing.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/components/remote-controls/grid/saved/impl/src/main/res/values/strings.xml b/components/remote-controls/grid/saved/impl/src/main/res/values/strings.xml new file mode 100644 index 0000000000..c409e90a45 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + + Page is empty! + Flipper not connected + Syncing… + Synced! + Remote Info + Rename + Share + Delete + \ No newline at end of file diff --git a/components/remote-controls/main/impl/build.gradle.kts b/components/remote-controls/main/impl/build.gradle.kts index 3895c2322c..9c8a8f5218 100644 --- a/components/remote-controls/main/impl/build.gradle.kts +++ b/components/remote-controls/main/impl/build.gradle.kts @@ -29,7 +29,7 @@ dependencies { implementation(projects.components.remoteControls.main.api) implementation(projects.components.remoteControls.brands.api) implementation(projects.components.remoteControls.categories.api) - implementation(projects.components.remoteControls.grid.api) + implementation(projects.components.remoteControls.grid.main.api) implementation(projects.components.remoteControls.setup.api) // Compose diff --git a/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/SaveTempSignalApi.kt b/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/SaveTempSignalApi.kt index ff3335a4b8..d3b0a9dffa 100644 --- a/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/SaveTempSignalApi.kt +++ b/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/SaveTempSignalApi.kt @@ -7,12 +7,14 @@ interface SaveTempSignalApi : InstanceKeeper.Instance { val state: StateFlow - fun saveFile( - textContent: String, - nameWithExtension: String, - extFolderPath: String + class FileDesc( + val textContent: String, + val nameWithExtension: String, + val extFolderPath: String ) + fun saveFiles(vararg filesDesc: FileDesc, onFinished: () -> Unit = {}) + sealed interface State { data object Pending : State data object Error : State diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/SetupScreen.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/SetupScreen.kt index 4ba4865013..6865e82b0b 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/SetupScreen.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/SetupScreen.kt @@ -17,9 +17,9 @@ import androidx.compose.ui.res.stringResource import com.flipperdevices.core.ui.dialog.composable.busy.ComposableFlipperBusy import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable -import com.flipperdevices.ifrmvp.core.ui.layout.shared.LoadingComposable import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar import com.flipperdevices.remotecontrols.impl.setup.composable.components.LoadedContent +import com.flipperdevices.remotecontrols.impl.setup.composable.components.SetupLoadingContent import com.flipperdevices.remotecontrols.impl.setup.presentation.decompose.SetupComponent import com.flipperdevices.rootscreen.api.LocalRootNavigation import com.flipperdevices.rootscreen.model.RootScreenConfig @@ -47,7 +47,7 @@ fun SetupScreen( LaunchedEffect(setupComponent.remoteFoundFlow) { setupComponent.remoteFoundFlow.onEach { setupComponent.onFileFound(it) - val configuration = RootScreenConfig.RemoteControlGrid.Id(it.id) + val configuration = RootScreenConfig.ServerRemoteControl(it.id) rootNavigation.push(configuration) }.launchIn(this) } @@ -93,7 +93,7 @@ fun SetupScreen( } is SetupComponent.Model.Loading -> { - LoadingComposable(progress = model.progress) + SetupLoadingContent() } } } diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/ButtonContent.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/ButtonContent.kt index c571d7e6fd..5318b00b0c 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/ButtonContent.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/ButtonContent.kt @@ -28,13 +28,15 @@ import com.flipperdevices.remotecontrols.setup.impl.R as SetupR private fun SignalResponseButton( data: ButtonData, onClick: () -> Unit, - emulatedKeyIdentifier: IfrKeyIdentifier? + emulatedKeyIdentifier: IfrKeyIdentifier?, + isSyncing: Boolean ) { ButtonItemComposable( buttonData = data, onKeyDataClick = { onClick.invoke() }, modifier = Modifier.size(64.dp), emulatedKeyIdentifier = emulatedKeyIdentifier, + isSyncing = isSyncing ) } @@ -42,6 +44,7 @@ private fun SignalResponseButton( fun ButtonContent( onClick: () -> Unit, data: ButtonData, + isSyncing: Boolean, emulatedKeyIdentifier: IfrKeyIdentifier?, categoryName: String, modifier: Modifier = Modifier, @@ -55,6 +58,7 @@ fun ButtonContent( data = data, onClick = onClick, emulatedKeyIdentifier = emulatedKeyIdentifier, + isSyncing = isSyncing ) Spacer(modifier = Modifier.height(14.dp)) Text( @@ -80,19 +84,22 @@ private fun ComposableConfirmContentDarkPreview() { onClick = {}, categoryName = "CATEGORY", data = TextButtonData(text = "Hello"), - emulatedKeyIdentifier = null + emulatedKeyIdentifier = null, + isSyncing = false ) ButtonContent( onClick = {}, categoryName = "CATEGORY 2", data = TextButtonData(text = "TV/AV"), - emulatedKeyIdentifier = null + emulatedKeyIdentifier = null, + isSyncing = false ) ButtonContent( onClick = {}, categoryName = "CATEGORY 2", data = TextButtonData(text = "Hello world"), - emulatedKeyIdentifier = null + emulatedKeyIdentifier = null, + isSyncing = false ) } } diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/LoadedContent.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/LoadedContent.kt index 56d4d8d449..26370a390d 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/LoadedContent.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/LoadedContent.kt @@ -39,7 +39,8 @@ fun LoadedContent( modifier = Modifier.align(Alignment.Center), data = signalResponse.data, categoryName = signalResponse.categoryName, - emulatedKeyIdentifier = model.emulatedKeyIdentifier + emulatedKeyIdentifier = model.emulatedKeyIdentifier, + isSyncing = model.isSyncing ) AnimatedVisibility( visible = model.isEmulated, @@ -80,7 +81,8 @@ private fun LoadedContentPreview() { model = SetupComponent.Model.Loaded( response = SignalResponseModel(), isEmulated = true, - emulatedKeyIdentifier = null + emulatedKeyIdentifier = null, + isSyncing = false ), onPositiveClick = {}, onNegativeClick = {}, diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/SetupLoadingContent.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/SetupLoadingContent.kt new file mode 100644 index 0000000000..8f5b7d763e --- /dev/null +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/composable/components/SetupLoadingContent.kt @@ -0,0 +1,47 @@ +package com.flipperdevices.remotecontrols.impl.setup.composable.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.placeholderConnecting +import com.flipperdevices.core.ui.theme.FlipperThemeInternal + +@Composable +internal fun SetupLoadingContent(modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(64.dp) + .placeholderConnecting() + ) + Spacer(modifier = Modifier.height(8.dp)) + Box( + modifier = Modifier + .height(16.dp) + .fillMaxWidth(fraction = 0.7f) + .placeholderConnecting() + ) + } +} + +@Preview +@Composable +private fun SetupLoadingContentPreview() { + FlipperThemeInternal { + SetupLoadingContent() + } +} diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/SetupComponent.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/SetupComponent.kt index d265346c8e..af6b9ac929 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/SetupComponent.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/SetupComponent.kt @@ -34,7 +34,8 @@ interface SetupComponent { val response: SignalResponseModel, val isFlipperBusy: Boolean = false, val emulatedKeyIdentifier: IfrKeyIdentifier?, - val isEmulated: Boolean + val isEmulated: Boolean, + val isSyncing: Boolean ) : Model data object Error : Model diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupComponentImpl.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupComponentImpl.kt index 8a1e7600a0..ccc484fbfd 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupComponentImpl.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupComponentImpl.kt @@ -66,10 +66,12 @@ class SetupComponentImpl @AssistedInject constructor( factory = { currentSignalViewModelFactory.invoke(param) { responseModel -> val signalModel = responseModel.signalResponse?.signalModel ?: return@invoke - saveSignalApi.saveFile( - textContent = signalModel.toFFFormat().openStream().reader().readText(), - nameWithExtension = TEMP_FILE_NAME, - extFolderPath = ABSOLUTE_TEMP_FOLDER_PATH + saveSignalApi.saveFiles( + SaveTempSignalApi.FileDesc( + textContent = signalModel.toFFFormat().openStream().reader().readText(), + nameWithExtension = TEMP_FILE_NAME, + extFolderPath = ABSOLUTE_TEMP_FOLDER_PATH + ) ) } } @@ -86,22 +88,14 @@ class SetupComponentImpl @AssistedInject constructor( is CurrentSignalViewModel.State.Loaded -> { when (saveState) { SaveTempSignalApi.State.Error -> SetupComponent.Model.Error + is SaveTempSignalApi.State.Uploading, + SaveTempSignalApi.State.Uploaded, SaveTempSignalApi.State.Pending -> SetupComponent.Model.Loaded( response = signalState.response, isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, emulatedKeyIdentifier = emulatingState?.ifrKeyIdentifier, - isEmulated = isEmulated - ) - - SaveTempSignalApi.State.Uploaded -> SetupComponent.Model.Loaded( - response = signalState.response, - isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, - emulatedKeyIdentifier = emulatingState?.ifrKeyIdentifier, - isEmulated = isEmulated - ) - - is SaveTempSignalApi.State.Uploading -> SetupComponent.Model.Loading( - saveState.progress + isEmulated = isEmulated, + isSyncing = saveState is SaveTempSignalApi.State.Uploading ) } } diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/SaveTempSignalViewModel.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/SaveTempSignalViewModel.kt index 184160d500..0bf8013520 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/SaveTempSignalViewModel.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/SaveTempSignalViewModel.kt @@ -1,8 +1,6 @@ package com.flipperdevices.remotecontrols.impl.setup.presentation.viewmodel import com.flipperdevices.bridge.dao.api.model.FlipperFilePath -import com.flipperdevices.bridge.service.api.FlipperServiceApi -import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceConsumer import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ktx.jre.FlipperDispatchers @@ -30,7 +28,6 @@ class SaveTempSignalViewModel @Inject constructor( private val saveFileApi: SaveFileApi, private val saveFolderApi: SaveFolderApi, ) : DecomposeViewModel(), - FlipperBleServiceConsumer, LogTagProvider, SaveTempSignalApi { private val mutex = Mutex() @@ -38,50 +35,42 @@ class SaveTempSignalViewModel @Inject constructor( private val _state = MutableStateFlow(SaveTempSignalApi.State.Pending) override val state = _state.asStateFlow() - override fun saveFile( - textContent: String, - nameWithExtension: String, - extFolderPath: String - ) = save( - extFolderPath = extFolderPath, - textContent = textContent, - absolutePath = FlipperFilePath( - folder = extFolderPath, - nameWithExtension = nameWithExtension - ).getPathOnFlipper(), - ) - - private fun save( - textContent: String, - absolutePath: String, - extFolderPath: String + override fun saveFiles( + vararg filesDesc: SaveTempSignalApi.FileDesc, + onFinished: () -> Unit, ) { viewModelScope.launch { _state.emit(SaveTempSignalApi.State.Uploading(0, 0)) val serviceApi = serviceProvider.getServiceApi() launchWithLock(mutex, viewModelScope, "load") { - saveFolderApi.save(serviceApi.requestApi, "$EXT_PATH/$extFolderPath") - val saveFileFlow = saveFileApi.save( - requestApi = serviceApi.requestApi, - textContent = textContent, - absolutePath = absolutePath - ) - saveFileFlow - .flowOn(FlipperDispatchers.workStealingDispatcher) - .onEach { - _state.value = when (it) { - SaveFileApi.Status.Finished -> SaveTempSignalApi.State.Uploaded - is SaveFileApi.Status.Saving -> SaveTempSignalApi.State.Uploading( - it.uploaded, - it.size - ) + filesDesc.forEach { fileDesc -> + val absolutePath = FlipperFilePath( + folder = fileDesc.extFolderPath, + nameWithExtension = fileDesc.nameWithExtension + ).getPathOnFlipper() + + saveFolderApi.save(serviceApi.requestApi, "$EXT_PATH/${fileDesc.extFolderPath}") + val saveFileFlow = saveFileApi.save( + requestApi = serviceApi.requestApi, + textContent = fileDesc.textContent, + absolutePath = absolutePath + ) + saveFileFlow + .flowOn(FlipperDispatchers.workStealingDispatcher) + .onEach { + _state.value = when (it) { + SaveFileApi.Status.Finished -> SaveTempSignalApi.State.Uploaded + is SaveFileApi.Status.Saving -> SaveTempSignalApi.State.Uploading( + it.uploaded, + it.size + ) + } } - } - .collect() + .collect() + } _state.value = SaveTempSignalApi.State.Uploaded + onFinished.invoke() } } } - - override fun onServiceApiReady(serviceApi: FlipperServiceApi) = Unit } diff --git a/components/rootscreen/api/src/main/kotlin/com/flipperdevices/rootscreen/model/RootScreenConfig.kt b/components/rootscreen/api/src/main/kotlin/com/flipperdevices/rootscreen/model/RootScreenConfig.kt index 6a5f8c10e9..5a349f56fc 100644 --- a/components/rootscreen/api/src/main/kotlin/com/flipperdevices/rootscreen/model/RootScreenConfig.kt +++ b/components/rootscreen/api/src/main/kotlin/com/flipperdevices/rootscreen/model/RootScreenConfig.kt @@ -39,8 +39,5 @@ sealed class RootScreenConfig { data object RemoteControls : RootScreenConfig() @Serializable - sealed class RemoteControlGrid : RootScreenConfig() { - data class Id(val ifrFileId: Long) : RemoteControlGrid() - data class Path(val flipperKeyPath: FlipperKeyPath) : RemoteControlGrid() - } + data class ServerRemoteControl(val infraredFileId: Long) : RootScreenConfig() } diff --git a/components/rootscreen/impl/build.gradle.kts b/components/rootscreen/impl/build.gradle.kts index 198ac3a620..509c65088a 100644 --- a/components/rootscreen/impl/build.gradle.kts +++ b/components/rootscreen/impl/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { implementation(projects.components.bridge.dao.api) implementation(projects.components.changelog.api) - implementation(projects.components.remoteControls.grid.api) + implementation(projects.components.remoteControls.grid.main.api) implementation(projects.components.remoteControls.main.api) implementation(projects.components.remoteControls.brands.api) diff --git a/components/rootscreen/impl/src/main/kotlin/com/flipperdevices/rootscreen/impl/api/RootDecomposeComponentImpl.kt b/components/rootscreen/impl/src/main/kotlin/com/flipperdevices/rootscreen/impl/api/RootDecomposeComponentImpl.kt index 2676eed79e..64c7b2a16c 100644 --- a/components/rootscreen/impl/src/main/kotlin/com/flipperdevices/rootscreen/impl/api/RootDecomposeComponentImpl.kt +++ b/components/rootscreen/impl/src/main/kotlin/com/flipperdevices/rootscreen/impl/api/RootDecomposeComponentImpl.kt @@ -10,7 +10,6 @@ import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.navigate -import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.pushToFront import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope @@ -23,8 +22,9 @@ import com.flipperdevices.faphub.screenshotspreview.api.ScreenshotsPreviewDecomp import com.flipperdevices.firstpair.api.FirstPairApi import com.flipperdevices.firstpair.api.FirstPairDecomposeComponent import com.flipperdevices.keyscreen.api.KeyScreenDecomposeComponent -import com.flipperdevices.remotecontrols.api.GridScreenDecomposeComponent +import com.flipperdevices.remotecontrols.api.ConfigureGridDecomposeComponent import com.flipperdevices.remotecontrols.api.RemoteControlsScreenDecomposeComponent +import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam import com.flipperdevices.rootscreen.api.RootDecomposeComponent import com.flipperdevices.rootscreen.impl.deeplink.RootDeeplinkHandler import com.flipperdevices.rootscreen.model.RootScreenConfig @@ -60,7 +60,7 @@ class RootDecomposeComponentImpl @AssistedInject constructor( private val screenshotsPreviewFactory: ScreenshotsPreviewDecomposeComponent.Factory, private val changelogScreenDecomposeFactory: ChangelogScreenDecomposeComponent.Factory, private val remoteControlsComponentFactory: RemoteControlsScreenDecomposeComponent.Factory, - private val gridScreenDecomposeComponentFactory: GridScreenDecomposeComponent.Factory + private val serverRemoteControlFactory: ConfigureGridDecomposeComponent.Factory ) : RootDecomposeComponent, ComponentContext by componentContext { private val scope = coroutineScope(FlipperDispatchers.workStealingDispatcher) private val navigation = StackNavigation() @@ -139,16 +139,10 @@ class RootDecomposeComponentImpl @AssistedInject constructor( onBack = this::internalOnBack ) - is RootScreenConfig.RemoteControlGrid.Id -> gridScreenDecomposeComponentFactory( + is RootScreenConfig.ServerRemoteControl -> serverRemoteControlFactory( componentContext = componentContext, - param = GridScreenDecomposeComponent.Param.Id(config.ifrFileId), - onPopClick = navigation::pop - ) - - is RootScreenConfig.RemoteControlGrid.Path -> gridScreenDecomposeComponentFactory( - componentContext = componentContext, - param = GridScreenDecomposeComponent.Param.Path(config.flipperKeyPath), - onPopClick = navigation::pop + param = ServerRemoteControlParam(config.infraredFileId), + onBack = this::internalOnBack, ) } diff --git a/components/share/uploader/src/main/kotlin/com/flipperdevices/uploader/viewmodel/UploaderViewModel.kt b/components/share/uploader/src/main/kotlin/com/flipperdevices/uploader/viewmodel/UploaderViewModel.kt index 1cba85401e..5d4d0d2cbe 100644 --- a/components/share/uploader/src/main/kotlin/com/flipperdevices/uploader/viewmodel/UploaderViewModel.kt +++ b/components/share/uploader/src/main/kotlin/com/flipperdevices/uploader/viewmodel/UploaderViewModel.kt @@ -142,8 +142,13 @@ class UploaderViewModel @AssistedInject constructor( val shadowFile = flipperKey .additionalFiles .firstOrNull { it.path.fileType == FlipperFileType.SHADOW_NFC } + val irUiFile = flipperKey + .additionalFiles + .firstOrNull { it.path.fileType == FlipperFileType.UI_INFRARED } if (flipperKey.flipperKeyType == FlipperKeyType.NFC && shadowFile != null) { return shadowFile.content + } else if (flipperKey.flipperKeyType == FlipperKeyType.INFRARED && irUiFile != null) { + return irUiFile.content } return flipperKey.mainFile.content diff --git a/instances/android/app/build.gradle.kts b/instances/android/app/build.gradle.kts index 9971e5edb1..05bb80dfe1 100644 --- a/instances/android/app/build.gradle.kts +++ b/instances/android/app/build.gradle.kts @@ -49,8 +49,14 @@ dependencies { implementation(projects.components.remoteControls.brands.impl) implementation(projects.components.remoteControls.categories.api) implementation(projects.components.remoteControls.categories.impl) - implementation(projects.components.remoteControls.grid.api) - implementation(projects.components.remoteControls.grid.impl) + implementation(projects.components.remoteControls.grid.createControl.api) + implementation(projects.components.remoteControls.grid.createControl.impl) + implementation(projects.components.remoteControls.grid.main.api) + implementation(projects.components.remoteControls.grid.main.impl) + implementation(projects.components.remoteControls.grid.remote.api) + implementation(projects.components.remoteControls.grid.remote.impl) + implementation(projects.components.remoteControls.grid.saved.api) + implementation(projects.components.remoteControls.grid.saved.impl) implementation(projects.components.remoteControls.main.api) implementation(projects.components.remoteControls.main.impl) implementation(projects.components.remoteControls.setup.api) diff --git a/settings.gradle.kts b/settings.gradle.kts index 8dd1ae1fde..15b94f07d3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -288,8 +288,14 @@ include( ":components:remote-controls:brands:api", ":components:remote-controls:categories:impl", ":components:remote-controls:categories:api", - ":components:remote-controls:grid:impl", - ":components:remote-controls:grid:api", + ":components:remote-controls:grid:main:impl", + ":components:remote-controls:grid:main:api", + ":components:remote-controls:grid:create-control:impl", + ":components:remote-controls:grid:create-control:api", + ":components:remote-controls:grid:remote:impl", + ":components:remote-controls:grid:remote:api", + ":components:remote-controls:grid:saved:impl", + ":components:remote-controls:grid:saved:api", ":components:remote-controls:setup:impl", ":components:remote-controls:setup:api",