From ff55dcd8e19b0094413f7c6a450eb2d2c356341a Mon Sep 17 00:00:00 2001 From: Roman Makeev <57789105+makeevrserg@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:16:48 +0000 Subject: [PATCH] Update remote controls (#910) **Background** This PR introduces update for remote controls with UI improvements and deeplink integration for local-flipper files **Changes** - Animated icons when emulating - Fix bottom navigation paddings - Save .ui.json file on flipper - Fix save api paths usage - Add Raw DeeplinkContent **Test plan** - Enable remote controls in debug options - Open infrareds list screen - Press add remote - Select some brand and complete configuration for some device - See grid screen, tap button and look at beautiful animation --- CHANGELOG.md | 1 + .../deeplink/model/DeeplinkContent.kt | 2 + .../brands/api/build.gradle.kts | 2 - .../api/BrandsScreenDecomposeComponent.kt | 2 +- .../composable/composable/BrandsList.kt | 2 + .../composable/alphabet/HeadersComposable.kt | 3 + .../categories/api/build.gradle.kts | 2 - .../api/CategoriesScreenDecomposeComponent.kt | 2 +- .../remote-controls/core-ui/build.gradle.kts | 2 + .../core/ui/button/Base64ImageButton.kt | 2 +- .../core/ui/button/ButtonItemComposable.kt | 9 ++ .../ifrmvp/core/ui/button/DoubleButton.kt | 14 ++- .../core/ui/button/core/SquareButton.kt | 18 ++- .../core/ui/button/core/SquareIconButton.kt | 29 ++++- .../ifrmvp/core/ui/button/core/TextButton.kt | 2 +- .../core/ui/layout/shared/SharedTopBar.kt | 91 +++++++++++++-- .../remote-controls/grid/api/build.gradle.kts | 2 +- .../api/GridScreenDecomposeComponent.kt | 14 ++- .../grid/impl/build.gradle.kts | 2 + .../impl/grid/composable/GridComposable.kt | 110 ++---------------- .../{ => components}/ButtonsComposable.kt | 4 +- .../components/GridComposableContent.kt | 65 +++++++++++ .../components/GridComposableLoadedContent.kt | 50 ++++++++ .../preview/LoadedContentPreview.kt | 7 +- .../composable/util/GridComponentModelExt.kt | 10 ++ .../data/localpages/LocalPagesRepository.kt | 13 +++ .../localpages/LocalPagesRepositoryImpl.kt | 40 +++++++ .../{ => pages}/BackendPagesRepository.kt | 3 +- .../data/{ => pages}/PagesRepository.kt | 2 +- .../presentation/decompose/GridComponent.kt | 5 +- .../decompose/internal/GridComponentImpl.kt | 101 ++++++++-------- .../mapping/GridComponentStateMapper.kt | 35 ++++++ .../grid/presentation/util/GridParamExt.kt | 31 +++++ .../presentation/viewmodel/GridViewModel.kt | 95 ++++++++++----- .../remote-controls/main/api/build.gradle.kts | 2 - .../RemoteControlsScreenDecomposeComponent.kt | 2 +- .../main/impl/build.gradle.kts | 1 - ...oteControlsScreenDecomposeComponentImpl.kt | 80 +++++-------- .../model/RemoteControlsNavigationConfig.kt | 3 - .../setup/api/build.gradle.kts | 2 - .../remotecontrols/api/DispatchSignalApi.kt | 4 +- .../remotecontrols/api/SaveTempSignalApi.kt | 7 +- .../api/SetupScreenDecomposeComponent.kt | 4 +- .../setup/impl/build.gradle.kts | 1 - .../impl/setup/api/save/file/SaveFileApi.kt | 3 +- .../setup/api/save/file/SaveFileApiImpl.kt | 17 ++- .../impl/setup/composable/SetupScreen.kt | 8 +- .../composable/components/ButtonContent.kt | 14 ++- .../composable/components/LoadedContent.kt | 8 +- .../presentation/decompose/SetupComponent.kt | 5 +- .../decompose/internal/SetupComponentImpl.kt | 25 ++-- .../SetupScreenDecomposeComponentImpl.kt | 4 +- .../viewmodel/DispatchSignalViewModel.kt | 6 +- .../viewmodel/SaveTempSignalViewModel.kt | 90 +++++++------- .../rootscreen/model/RootScreenConfig.kt | 6 + components/rootscreen/impl/build.gradle.kts | 1 + .../impl/api/RootDecomposeComponentImpl.kt | 23 +++- 57 files changed, 709 insertions(+), 379 deletions(-) rename components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/{ => components}/ButtonsComposable.kt (93%) create mode 100644 components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableContent.kt create mode 100644 components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableLoadedContent.kt create mode 100644 components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/util/GridComponentModelExt.kt create mode 100644 components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepository.kt create mode 100644 components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepositoryImpl.kt rename components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/{ => pages}/BackendPagesRepository.kt (98%) rename components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/{ => pages}/PagesRepository.kt (96%) create mode 100644 components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/mapping/GridComponentStateMapper.kt create mode 100644 components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/util/GridParamExt.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b70cd9c1..3dbb6dbd0d 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 +- [Refactor] Load RemoteControls from flipper, emulating animation - [Refactor] Update to Kotlin 2.0 - [Refactor] Replace Ktorfit with Ktor requests in remote-controls - [Refactor] Migrate :core:preference to KMP diff --git a/components/deeplink/api/src/main/java/com/flipperdevices/deeplink/model/DeeplinkContent.kt b/components/deeplink/api/src/main/java/com/flipperdevices/deeplink/model/DeeplinkContent.kt index 76c9872f84..8e8f0f90dd 100644 --- a/components/deeplink/api/src/main/java/com/flipperdevices/deeplink/model/DeeplinkContent.kt +++ b/components/deeplink/api/src/main/java/com/flipperdevices/deeplink/model/DeeplinkContent.kt @@ -6,6 +6,7 @@ import android.net.Uri import android.os.Parcelable import com.flipperdevices.bridge.dao.api.model.FlipperFileFormat import com.flipperdevices.bridge.dao.api.model.FlipperKeyCrypto +import com.flipperdevices.core.ktx.jre.length import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -81,6 +82,7 @@ sealed class DeeplinkContent : Parcelable { uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) + is InternalStorageFile -> file.delete() is FFFContent -> {} // Nothing is FFFCryptoContent -> {} // Nothing diff --git a/components/remote-controls/brands/api/build.gradle.kts b/components/remote-controls/brands/api/build.gradle.kts index 207e1f6692..dd3797f7b8 100644 --- a/components/remote-controls/brands/api/build.gradle.kts +++ b/components/remote-controls/brands/api/build.gradle.kts @@ -6,8 +6,6 @@ plugins { android.namespace = "com.flipperdevices.remotecontrols.brands.api" dependencies { - implementation(projects.components.deeplink.api) - implementation(projects.components.core.ui.decompose) implementation(libs.compose.ui) diff --git a/components/remote-controls/brands/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/BrandsScreenDecomposeComponent.kt b/components/remote-controls/brands/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/BrandsScreenDecomposeComponent.kt index a4a81e8ecc..f2435f5430 100644 --- a/components/remote-controls/brands/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/BrandsScreenDecomposeComponent.kt +++ b/components/remote-controls/brands/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/BrandsScreenDecomposeComponent.kt @@ -8,7 +8,7 @@ abstract class BrandsScreenDecomposeComponent( ) : ScreenDecomposeComponent(componentContext) { fun interface Factory { - fun createBrandsComponent( + operator fun invoke( componentContext: ComponentContext, categoryId: Long, onBackClick: () -> Unit, diff --git a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/BrandsList.kt b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/BrandsList.kt index 8c7c87d996..5353e81a36 100644 --- a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/BrandsList.kt +++ b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/BrandsList.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -68,5 +69,6 @@ fun BrandsList( ) } } + item { Spacer(Modifier.navigationBarsPadding()) } } } diff --git a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/alphabet/HeadersComposable.kt b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/alphabet/HeadersComposable.kt index 71cf6e62f3..fd82de27d9 100644 --- a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/alphabet/HeadersComposable.kt +++ b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/composable/composable/alphabet/HeadersComposable.kt @@ -6,7 +6,9 @@ import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -93,5 +95,6 @@ internal fun HeadersComposable( } ) } + Spacer(Modifier.navigationBarsPadding()) } } diff --git a/components/remote-controls/categories/api/build.gradle.kts b/components/remote-controls/categories/api/build.gradle.kts index 3d43c9379f..8965600252 100644 --- a/components/remote-controls/categories/api/build.gradle.kts +++ b/components/remote-controls/categories/api/build.gradle.kts @@ -5,8 +5,6 @@ plugins { android.namespace = "com.flipperdevices.remotecontrols.categories.api" dependencies { - implementation(projects.components.deeplink.api) - implementation(projects.components.core.ui.decompose) implementation(libs.compose.ui) diff --git a/components/remote-controls/categories/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/CategoriesScreenDecomposeComponent.kt b/components/remote-controls/categories/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/CategoriesScreenDecomposeComponent.kt index c1ad36ad93..5fec769390 100644 --- a/components/remote-controls/categories/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/CategoriesScreenDecomposeComponent.kt +++ b/components/remote-controls/categories/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/CategoriesScreenDecomposeComponent.kt @@ -8,7 +8,7 @@ abstract class CategoriesScreenDecomposeComponent( ) : ScreenDecomposeComponent(componentContext) { fun interface Factory { - fun invoke( + operator fun invoke( componentContext: ComponentContext, onBackClick: () -> Unit, onCategoryClick: (categoryId: Long) -> Unit diff --git a/components/remote-controls/core-ui/build.gradle.kts b/components/remote-controls/core-ui/build.gradle.kts index 6f760cb8ef..fca6ec02ba 100644 --- a/components/remote-controls/core-ui/build.gradle.kts +++ b/components/remote-controls/core-ui/build.gradle.kts @@ -17,9 +17,11 @@ dependencies { // Compose implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.tooling) implementation(libs.compose.material) implementation(libs.compose.material.icons.core) implementation(libs.compose.material.icons.extended) + implementation(libs.compose.placeholder) implementation(libs.bundles.decompose) } 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 a6ef983820..43599f5db9 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,8 +64,8 @@ fun rememberImageBitmap(base64Image: String): ImageBitmap? { @Composable fun Base64ImageButton( base64Icon: String, + isEmulating: Boolean, modifier: Modifier = Modifier, - isEmulating: Boolean = false, onClick: () -> Unit ) { val imageBitmap = rememberImageBitmap(base64Icon) 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 cb46bcf751..29ba3e6865 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 @@ -15,9 +15,11 @@ import com.flipperdevices.ifrmvp.model.buttondata.TextButtonData import com.flipperdevices.ifrmvp.model.buttondata.UnknownButtonData import com.flipperdevices.ifrmvp.model.buttondata.VolumeButtonData +@Suppress("LongMethod") @Composable fun ButtonItemComposable( buttonData: ButtonData, + emulatedKeyIdentifier: IfrKeyIdentifier?, onKeyDataClick: (IfrKeyIdentifier) -> Unit, modifier: Modifier = Modifier ) { @@ -26,6 +28,7 @@ fun ButtonItemComposable( SquareIconButton( iconType = buttonData.iconId, modifier = modifier, + isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) } ) } @@ -35,6 +38,8 @@ fun ButtonItemComposable( onNextClick = { onKeyDataClick.invoke(buttonData.addKeyIdentifier) }, onPrevClick = { onKeyDataClick.invoke(buttonData.reduceKeyIdentifier) }, modifier = modifier, + isEmulating = buttonData.reduceKeyIdentifier == emulatedKeyIdentifier || + buttonData.addKeyIdentifier == emulatedKeyIdentifier ) } @@ -43,6 +48,8 @@ fun ButtonItemComposable( onAddClick = { onKeyDataClick.invoke(buttonData.addKeyIdentifier) }, onReduceClick = { onKeyDataClick.invoke(buttonData.reduceKeyIdentifier) }, modifier = modifier, + isEmulating = buttonData.reduceKeyIdentifier == emulatedKeyIdentifier || + buttonData.addKeyIdentifier == emulatedKeyIdentifier ) } @@ -62,6 +69,7 @@ fun ButtonItemComposable( onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) }, text = buttonData.text, background = LocalPalletV2.current.surface.menu.body.dufault, + isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, modifier = modifier, ) } @@ -71,6 +79,7 @@ fun ButtonItemComposable( base64Icon = buttonData.pngBase64, onClick = { onKeyDataClick.invoke(buttonData.keyIdentifier) }, modifier = modifier, + isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, ) } 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 44fc4c4cf9..a11c553e23 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 @@ -18,6 +18,7 @@ fun DoubleButton( onLastClick: () -> Unit, firstText: String, lastText: String, + isEmulating: Boolean, modifier: Modifier = Modifier, text: String? = null, ) { @@ -32,18 +33,21 @@ 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 + background = LocalPalletV2.current.surface.menu.body.dufault, + isEmulating = isEmulating ) } TextButton( onClick = onLastClick, text = lastText, background = LocalPalletV2.current.surface.menu.body.dufault, + isEmulating = isEmulating ) } } @@ -52,6 +56,7 @@ fun DoubleButton( fun VolumeButton( onAddClick: () -> Unit, onReduceClick: () -> Unit, + isEmulating: Boolean, modifier: Modifier = Modifier ) { DoubleButton( @@ -60,7 +65,8 @@ fun VolumeButton( text = "VOL", firstText = "+", lastText = "-", - modifier = modifier + modifier = modifier, + isEmulating = isEmulating ) } @@ -68,6 +74,7 @@ fun VolumeButton( fun ChannelButton( onNextClick: () -> Unit, onPrevClick: () -> Unit, + isEmulating: Boolean, modifier: Modifier = Modifier, ) { DoubleButton( @@ -76,6 +83,7 @@ fun ChannelButton( text = "CH", firstText = "+", lastText = "-", - modifier = modifier + modifier = modifier, + isEmulating = isEmulating ) } 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 c631bfa100..7e38c4cd03 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 @@ -1,9 +1,11 @@ package com.flipperdevices.ifrmvp.core.ui.button.core +import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -11,6 +13,7 @@ import androidx.compose.ui.Alignment 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.ifrmvp.core.ui.layout.core.sf import com.flipperdevices.ifrmvp.core.ui.util.GridConstants @@ -18,8 +21,8 @@ import com.flipperdevices.ifrmvp.core.ui.util.GridConstants fun SquareButton( onClick: (() -> Unit)?, background: Color, + isEmulating: Boolean, modifier: Modifier = Modifier, - isEmulating: Boolean = false, content: @Composable BoxScope.() -> Unit, ) { Box( @@ -38,6 +41,17 @@ fun SquareButton( } ), contentAlignment = Alignment.Center, - content = content + content = { + content.invoke(this) + Crossfade(isEmulating) { isEmulating -> + if (isEmulating) { + 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 d234886131..998b60d4e8 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 @@ -1,5 +1,6 @@ package com.flipperdevices.ifrmvp.core.ui.button.core +import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Icon @@ -8,6 +9,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.tooling.preview.Preview +import com.flipperdevices.core.ui.theme.FlipperThemeInternal import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.ifrmvp.core.ui.ext.asPainter import com.flipperdevices.ifrmvp.core.ui.ext.tintFor @@ -34,7 +37,9 @@ fun SquareIconButton( painter = painter, contentDescription = contentDescription, tint = iconTint, - modifier = Modifier.fillMaxSize().padding(12.sf) + modifier = Modifier + .fillMaxSize() + .padding(12.sf) ) } } @@ -59,7 +64,9 @@ fun SquareImageButton( bitmap = bitmap, contentDescription = contentDescription, tint = iconTint, - modifier = Modifier.fillMaxSize().padding(12.sf) + modifier = Modifier + .fillMaxSize() + .padding(12.sf) ) } } @@ -67,6 +74,7 @@ fun SquareImageButton( @Composable fun SquareIconButton( iconType: IconButtonData.IconType, + isEmulating: Boolean, modifier: Modifier = Modifier, contentDescription: String? = null, onClick: () -> Unit, @@ -74,13 +82,28 @@ fun SquareIconButton( SquareButton( modifier = modifier, onClick = onClick, + isEmulating = isEmulating, background = LocalPalletV2.current.surface.menu.body.dufault ) { Icon( painter = iconType.asPainter(), contentDescription = contentDescription, tint = iconType.tintFor(), - modifier = Modifier.fillMaxSize().padding(12.sf) + modifier = Modifier + .fillMaxSize() + .padding(12.sf) + ) + } +} + +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun SquareIconButtonPreview() { + FlipperThemeInternal { + SquareIconButton( + iconType = IconButtonData.IconType.POWER, + isEmulating = true, + 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 79dcbf09ac..0c439004db 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,10 +12,10 @@ 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, - isEmulating: Boolean = false, onClick: (() -> Unit)? ) { SquareButton( 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 4d98ad6d37..b1401d2758 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 @@ -2,9 +2,9 @@ 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.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -17,18 +17,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.FlipperThemeInternal import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.core.ui.theme.LocalTypography import com.flipperdevices.core.ui.res.R as DesignSystem @Composable fun SharedTopBar( - title: String, - subtitle: String, onBackClick: () -> Unit, modifier: Modifier = Modifier, + title: String = "", + subtitle: String = "", + actions: @Composable () -> Unit = {} ) { Row( modifier = modifier @@ -39,18 +42,24 @@ fun SharedTopBar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Icon( - modifier = Modifier - .size(24.dp) - .clickableRipple(bounded = false, onClick = onBackClick), - painter = painterResource(DesignSystem.drawable.ic_back), - contentDescription = null, - tint = LocalPalletV2.current.icon.blackAndWhite.blackOnColor + Box( + modifier = Modifier.weight(weight = 1f), + contentAlignment = Alignment.CenterStart, + content = { + Icon( + modifier = Modifier + .size(24.dp) + .clickableRipple(bounded = false, onClick = onBackClick), + painter = painterResource(DesignSystem.drawable.ic_back), + contentDescription = null, + tint = LocalPalletV2.current.icon.blackAndWhite.blackOnColor + ) + } ) Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .weight(1f) + .weight(weight = 3f, fill = false) .padding(horizontal = 8.dp) ) { Text( @@ -68,6 +77,64 @@ fun SharedTopBar( style = LocalTypography.current.subtitleM12 ) } - Spacer(modifier = Modifier.size(24.dp)) + Box( + modifier = Modifier.weight(weight = 1f), + contentAlignment = Alignment.CenterEnd, + content = { + actions.invoke() + } + ) + } +} + +@Preview +@Composable +private fun SharedTopBarPreview() { + FlipperThemeInternal { + Column { + SharedTopBar( + title = "Title Title Title Title Title Title Title", + subtitle = "Subtitle", + onBackClick = {}, + ) + SharedTopBar( + title = "Title Title Title Title Title Title Title", + subtitle = "Subtitle", + onBackClick = {}, + actions = { + Row { + repeat(2) { + Icon( + modifier = Modifier + .size(24.dp) + .clickableRipple(bounded = false, onClick = {}), + painter = painterResource(DesignSystem.drawable.ic_back), + contentDescription = null, + tint = LocalPalletV2.current.icon.blackAndWhite.blackOnColor + ) + } + } + } + ) + SharedTopBar( + title = "Title Title Title Title Title Title Title", + 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 + ) + } + } + } + ) + } } } diff --git a/components/remote-controls/grid/api/build.gradle.kts b/components/remote-controls/grid/api/build.gradle.kts index 2c33b2bd50..3121b13492 100644 --- a/components/remote-controls/grid/api/build.gradle.kts +++ b/components/remote-controls/grid/api/build.gradle.kts @@ -5,7 +5,7 @@ plugins { android.namespace = "com.flipperdevices.remotecontrols.grid.api" dependencies { - implementation(projects.components.deeplink.api) + implementation(projects.components.bridge.dao.api) implementation(projects.components.core.ui.decompose) diff --git a/components/remote-controls/grid/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/GridScreenDecomposeComponent.kt b/components/remote-controls/grid/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/GridScreenDecomposeComponent.kt index 50bc48fc69..ce873335e1 100644 --- a/components/remote-controls/grid/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/GridScreenDecomposeComponent.kt +++ b/components/remote-controls/grid/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/GridScreenDecomposeComponent.kt @@ -1,6 +1,7 @@ package com.flipperdevices.remotecontrols.api import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath import com.flipperdevices.ui.decompose.ScreenDecomposeComponent abstract class GridScreenDecomposeComponent( @@ -8,15 +9,18 @@ abstract class GridScreenDecomposeComponent( ) : ScreenDecomposeComponent(componentContext) { fun interface Factory { - fun invoke( + operator fun invoke( componentContext: ComponentContext, param: Param, onPopClick: () -> Unit ): GridScreenDecomposeComponent } - class Param( - val ifrFileId: Long, - val uiFileId: Long? = null - ) + sealed interface Param { + data class Id(val irFileId: Long) : Param + data class Path(val flipperKeyPath: FlipperKeyPath) : Param + + val key: String + get() = this.toString() + } } diff --git a/components/remote-controls/grid/impl/build.gradle.kts b/components/remote-controls/grid/impl/build.gradle.kts index b58ad3b241..610babfc5d 100644 --- a/components/remote-controls/grid/impl/build.gradle.kts +++ b/components/remote-controls/grid/impl/build.gradle.kts @@ -40,6 +40,8 @@ dependencies { implementation(libs.kotlin.immutable.collections) + implementation(libs.kotlin.serialization.json) + implementation(libs.decompose) implementation(libs.bundles.decompose) 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 index 0d63d21581..fc575fb54b 100644 --- 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 @@ -1,11 +1,5 @@ package com.flipperdevices.remotecontrols.impl.grid.composable -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.material.rememberScaffoldState @@ -14,57 +8,17 @@ 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.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.flipperdevices.core.ui.dialog.composable.busy.ComposableFlipperBusy -import com.flipperdevices.core.ui.theme.FlipperThemeInternal 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.ifrmvp.model.IfrButton -import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier -import com.flipperdevices.ifrmvp.model.PagesLayout +import com.flipperdevices.remotecontrols.impl.grid.composable.components.GridComposableContent import com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.GridComponent -import com.flipperdevices.rootscreen.api.LocalRootNavigation -import com.flipperdevices.rootscreen.model.RootScreenConfig -import com.flipperdevices.remotecontrols.grid.impl.R as GridR - -@Composable -internal fun LoadedContent( - pagesLayout: PagesLayout, - onButtonClick: (IfrButton, IfrKeyIdentifier) -> Unit, - onReload: () -> Unit, - modifier: Modifier = Modifier, -) { - BoxWithConstraints( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.TopStart, - content = { - ButtonsComposable( - pageLayout = pagesLayout.pages.firstOrNull(), - onButtonClick = onButtonClick, - onReload = onReload - ) - } - ) -} - -private val GridComponent.Model.key: Any - get() = when (this) { - GridComponent.Model.Error -> "error" - is GridComponent.Model.Loaded -> "loaded" - is GridComponent.Model.Loading -> "loading" - } @Composable fun GridComposable( gridComponent: GridComponent, modifier: Modifier = Modifier ) { - val rootNavigation = LocalRootNavigation.current val coroutineScope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() val model by remember(gridComponent, coroutineScope) { @@ -74,67 +28,17 @@ fun GridComposable( modifier = modifier, topBar = { SharedTopBar( - title = "", - subtitle = "", - onBackClick = gridComponent::pop + onBackClick = gridComponent::pop, ) }, backgroundColor = LocalPalletV2.current.surface.backgroundMain.body, scaffoldState = scaffoldState, content = { scaffoldPaddings -> - AnimatedContent( - targetState = model, - modifier = Modifier.padding(scaffoldPaddings), - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - contentKey = { it.key } - ) { model -> - when (model) { - GridComponent.Model.Error -> { - ErrorComposable( - desc = stringResource(GridR.string.empty_page), - onReload = gridComponent::tryLoad - ) - } - - is GridComponent.Model.Loaded -> { - if (model.isFlipperBusy) { - ComposableFlipperBusy( - onDismiss = gridComponent::dismissBusyDialog, - goToRemote = { - gridComponent.dismissBusyDialog() - rootNavigation.push(RootScreenConfig.ScreenStreaming) - } - ) - } - LoadedContent( - pagesLayout = model.pagesLayout, - onButtonClick = { _, keyIdentifier -> - gridComponent.onButtonClick(keyIdentifier) - }, - onReload = gridComponent::tryLoad - ) - } - - is GridComponent.Model.Loading -> { - LoadingComposable(progress = model.progress) - } - } - } + GridComposableContent( + gridComponent = gridComponent, + model = model, + modifier = Modifier.padding(scaffoldPaddings) + ) } ) } - -@Preview( - showSystemUi = true, - showBackground = true -) -@Composable -private fun LoadedContentEmptyPreview() { - FlipperThemeInternal { - LoadedContent( - pagesLayout = PagesLayout(emptyList()), - onButtonClick = { _, _ -> }, - onReload = {} - ) - } -} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/ButtonsComposable.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/ButtonsComposable.kt similarity index 93% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/ButtonsComposable.kt rename to components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/ButtonsComposable.kt index a904223834..a20e56276a 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/ButtonsComposable.kt +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/ButtonsComposable.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.composable +package com.flipperdevices.remotecontrols.impl.grid.composable.components import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraintsScope @@ -25,6 +25,7 @@ import com.flipperdevices.remotecontrols.grid.impl.R as GridR @Composable fun BoxWithConstraintsScope.ButtonsComposable( pageLayout: PageLayout?, + emulatedKeyIdentifier: IfrKeyIdentifier?, onButtonClick: (IfrButton, IfrKeyIdentifier) -> Unit, onReload: () -> Unit, modifier: Modifier = Modifier @@ -57,6 +58,7 @@ fun BoxWithConstraintsScope.ButtonsComposable( content = { ButtonItemComposable( buttonData = button.data, + emulatedKeyIdentifier = emulatedKeyIdentifier, onKeyDataClick = { keyIdentifier -> onButtonClick.invoke(button, keyIdentifier) } 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/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableContent.kt new file mode 100644 index 0000000000..709e9ff3e6 --- /dev/null +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableContent.kt @@ -0,0 +1,65 @@ +package com.flipperdevices.remotecontrols.impl.grid.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 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.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.rootscreen.api.LocalRootNavigation +import com.flipperdevices.rootscreen.model.RootScreenConfig + +@Composable +internal fun GridComposableContent( + gridComponent: GridComponent, + model: GridComponent.Model, + modifier: Modifier = Modifier +) { + val rootNavigation = LocalRootNavigation.current + AnimatedContent( + targetState = model, + modifier = modifier, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + contentKey = { it.contentKey } + ) { animatedModel -> + when (animatedModel) { + GridComponent.Model.Error -> { + ErrorComposable( + desc = stringResource(R.string.empty_page), + onReload = gridComponent::tryLoad + ) + } + + is GridComponent.Model.Loaded -> { + if (animatedModel.isFlipperBusy) { + ComposableFlipperBusy( + onDismiss = gridComponent::dismissBusyDialog, + goToRemote = { + gridComponent.dismissBusyDialog() + rootNavigation.push(RootScreenConfig.ScreenStreaming) + } + ) + } + GridComposableLoadedContent( + pagesLayout = animatedModel.pagesLayout, + onButtonClick = { _, keyIdentifier -> + gridComponent.onButtonClick(keyIdentifier) + }, + onReload = gridComponent::tryLoad, + emulatedKeyIdentifier = animatedModel.emulatedKey + ) + } + + is GridComponent.Model.Loading -> { + LoadingComposable(progress = animatedModel.progress) + } + } + } +} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableLoadedContent.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableLoadedContent.kt new file mode 100644 index 0000000000..c495350ea3 --- /dev/null +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/components/GridComposableLoadedContent.kt @@ -0,0 +1,50 @@ +package com.flipperdevices.remotecontrols.impl.grid.composable.components + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.flipperdevices.core.ui.theme.FlipperThemeInternal +import com.flipperdevices.ifrmvp.model.IfrButton +import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier +import com.flipperdevices.ifrmvp.model.PagesLayout + +@Composable +internal fun GridComposableLoadedContent( + pagesLayout: PagesLayout, + onButtonClick: (IfrButton, IfrKeyIdentifier) -> Unit, + onReload: () -> Unit, + emulatedKeyIdentifier: IfrKeyIdentifier?, + modifier: Modifier = Modifier, +) { + BoxWithConstraints( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.TopStart, + content = { + ButtonsComposable( + pageLayout = pagesLayout.pages.firstOrNull(), + emulatedKeyIdentifier = emulatedKeyIdentifier, + onButtonClick = onButtonClick, + onReload = onReload + ) + } + ) +} + +@Preview( + showSystemUi = true, + showBackground = true +) +@Composable +private fun LoadedContentEmptyPreview() { + FlipperThemeInternal { + GridComposableLoadedContent( + pagesLayout = PagesLayout(emptyList()), + onButtonClick = { _, _ -> }, + onReload = {}, + emulatedKeyIdentifier = null + ) + } +} diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/LoadedContentPreview.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/LoadedContentPreview.kt index aefc2a4373..fb1ea453cb 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/LoadedContentPreview.kt +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/preview/LoadedContentPreview.kt @@ -3,7 +3,7 @@ package com.flipperdevices.remotecontrols.impl.grid.composable.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.LoadedContent +import com.flipperdevices.remotecontrols.impl.grid.composable.components.GridComposableLoadedContent @Preview( showSystemUi = true, @@ -12,10 +12,11 @@ import com.flipperdevices.remotecontrols.impl.grid.composable.LoadedContent @Composable private fun LoadedContentPreview() { FlipperThemeInternal { - LoadedContent( + GridComposableLoadedContent( pagesLayout = KitchenLayoutFactory.create(), onButtonClick = { _, _ -> }, - onReload = {} + onReload = {}, + emulatedKeyIdentifier = null ) } } 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 new file mode 100644 index 0000000000..8444d20112 --- /dev/null +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/composable/util/GridComponentModelExt.kt @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000000..f36aba3d5b --- /dev/null +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepository.kt @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000000..75842e5ba3 --- /dev/null +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/localpages/LocalPagesRepositoryImpl.kt @@ -0,0 +1,40 @@ +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/data/BackendPagesRepository.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/BackendPagesRepository.kt similarity index 98% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/BackendPagesRepository.kt rename to components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/BackendPagesRepository.kt index 4e2a029d0f..cb162ce4a2 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/BackendPagesRepository.kt +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/BackendPagesRepository.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.data +package com.flipperdevices.remotecontrols.impl.grid.presentation.data.pages import com.flipperdevices.core.di.AppGraph import com.flipperdevices.ifrmvp.api.infrared.InfraredBackendApi @@ -11,7 +11,6 @@ import javax.inject.Inject class BackendPagesRepository @Inject constructor( private val infraredBackendApi: InfraredBackendApi, ) : PagesRepository { - override suspend fun fetchDefaultPageLayout( ifrFileId: Long ): Result = runCatching { diff --git a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/PagesRepository.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/PagesRepository.kt similarity index 96% rename from components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/PagesRepository.kt rename to components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/PagesRepository.kt index ab574f384c..7e2573bbfc 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/PagesRepository.kt +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/data/pages/PagesRepository.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.remotecontrols.impl.grid.presentation.data +package com.flipperdevices.remotecontrols.impl.grid.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/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/GridComponent.kt index 6b3ef66f2d..7a9d3d6061 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/GridComponent.kt +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/decompose/GridComponent.kt @@ -14,20 +14,21 @@ interface GridComponent { fun onButtonClick(identifier: IfrKeyIdentifier) fun tryLoad() + fun pop() fun dismissBusyDialog() sealed interface Model { data class Loading( - val progress: Float, + val progress: Float = 0f, ) : Model data class Loaded( val pagesLayout: PagesLayout, val remotes: ImmutableList, val isFlipperBusy: Boolean = false, - val isEmulating: Boolean = false + val emulatedKey: IfrKeyIdentifier? = null, ) : Model data object Error : Model 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 index b7fb28302a..56a3a58872 100644 --- 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 @@ -2,15 +2,17 @@ package com.flipperdevices.remotecontrols.impl.grid.presentation.decompose.inter import com.arkivanov.decompose.ComponentContext import com.arkivanov.essenty.instancekeeper.getOrCreate -import com.flipperdevices.bridge.dao.api.model.FlipperFileFormat import com.flipperdevices.bridge.dao.api.model.FlipperFilePath -import com.flipperdevices.bridge.dao.api.model.FlipperKeyType 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 @@ -27,86 +29,81 @@ class GridComponentImpl @AssistedInject constructor( @Assisted private val param: GridScreenDecomposeComponent.Param, @Assisted private val onPopClick: () -> Unit, createGridViewModel: GridViewModel.Factory, - createSaveSignalViewModel: Provider, - createDispatchSignalViewModel: Provider + createSaveTempSignalApi: Provider, + createDispatchSignalApi: Provider ) : GridComponent, ComponentContext by componentContext { - private val saveSignalViewModel = instanceKeeper.getOrCreate( - key = "GridComponent_saveSignalViewModel_${param.ifrFileId}_${param.uiFileId}", + private val saveTempSignalApi = instanceKeeper.getOrCreate( + key = "GridComponent_saveSignalViewModel_${param.key}", factory = { - createSaveSignalViewModel.get() + createSaveTempSignalApi.get() } ) - private val dispatchSignalViewModel = instanceKeeper.getOrCreate( - key = "GridComponent_dispatchSignalViewModel_${param.ifrFileId}_${param.uiFileId}", + private val dispatchSignalApi = instanceKeeper.getOrCreate( + key = "GridComponent_dispatchSignalViewModel_${param.key}", factory = { - createDispatchSignalViewModel.get() + createDispatchSignalApi.get() } ) - private val gridFeature = instanceKeeper.getOrCreate( - key = "GridComponent_gridFeature_${param.ifrFileId}_${param.uiFileId}", + private val gridViewModel = instanceKeeper.getOrCreate( + key = "GridComponent_gridFeature_${param.key}", factory = { createGridViewModel.invoke( param = param, - onIrFileLoaded = { content -> - val fff = FlipperFileFormat.fromFileContent(content) - saveSignalViewModel.saveTempFile( - fff = fff, - nameWithExtension = "${param.ifrFileId}.ir" - ) + 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( - saveSignalViewModel.state, - gridFeature.state, - dispatchSignalViewModel.state, + saveTempSignalApi.state, + gridViewModel.state, + dispatchSignalApi.state, transform = { saveState, gridState, dispatchState -> - 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, - isEmulating = dispatchState is DispatchSignalApi.State.Emulating - ) - } - - is SaveTempSignalApi.State.Uploading -> GridComponent.Model.Loading( - saveState.progress - ) - } - } - - GridViewModel.State.Loading -> GridComponent.Model.Loading(0f) - } + GridComponentStateMapper.map( + saveState = saveState, + gridState = gridState, + dispatchState = dispatchState + ) } - ).stateIn(coroutineScope, SharingStarted.Eagerly, GridComponent.Model.Loading(0f)) + ).stateIn(coroutineScope, SharingStarted.Eagerly, GridComponent.Model.Loading()) override fun dismissBusyDialog() { - dispatchSignalViewModel.dismissBusyDialog() + dispatchSignalApi.dismissBusyDialog() } override fun onButtonClick(identifier: IfrKeyIdentifier) { - val gridLoadedState = (gridFeature.state.value as? GridViewModel.State.Loaded) ?: return + val gridLoadedState = (gridViewModel.state.value as? GridViewModel.State.Loaded) ?: return val remotes = gridLoadedState.remotes - dispatchSignalViewModel.dispatch( + dispatchSignalApi.dispatch( identifier = identifier, remotes = remotes, ffPath = FlipperFilePath( - folder = FLIPPER_TEMP_FOLDER, - nameWithExtension = "${param.ifrFileId}.ir" + folder = param.extFolderPath, + nameWithExtension = param.nameWithExtension ) ) } - override fun tryLoad() = gridFeature.tryLoad() + override fun tryLoad() = gridViewModel.tryLoad() override fun pop() = onPopClick.invoke() } - -private val FLIPPER_TEMP_FOLDER = FlipperKeyType.INFRARED.flipperDir + "/temp" 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 new file mode 100644 index 0000000000..eaf8eebb07 --- /dev/null +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/mapping/GridComponentStateMapper.kt @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000000..b445c93875 --- /dev/null +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/util/GridParamExt.kt @@ -0,0 +1,31 @@ +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/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/viewmodel/GridViewModel.kt b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/viewmodel/GridViewModel.kt index fbd00191cd..44a2b3b278 100644 --- a/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/viewmodel/GridViewModel.kt +++ b/components/remote-controls/grid/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/presentation/viewmodel/GridViewModel.kt @@ -8,7 +8,10 @@ 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.PagesRepository +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 dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -17,44 +20,71 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +@Suppress("LongParameterList") class GridViewModel @AssistedInject constructor( private val pagesRepository: PagesRepository, + private val localPagesRepository: LocalPagesRepository, @Assisted private val param: GridScreenDecomposeComponent.Param, - @Assisted private val onIrFileLoaded: (String) -> Unit + @Assisted private val onCallback: (Callback) -> Unit, ) : DecomposeViewModel(), LogTagProvider { override val TAG: String = "GridViewModel" - private val _state = MutableStateFlow(State.Loading) - val state = _state.asStateFlow() - private suspend fun loadIrFileContent(): List { - return pagesRepository.fetchKeyContent(param.ifrFileId) - .onFailure { _state.emit(State.Error) } - .onFailure { throwable -> error(throwable) { "#tryLoad could not load key content" } } - .onSuccess(onIrFileLoaded) - .map(FlipperFileFormat::fromFileContent) - .map(InfraredKeyParser::mapParsedKeyToInfraredRemotes) - .getOrNull() - .orEmpty() + private val json: Json = Json { + prettyPrint = false + ignoreUnknownKeys = true + isLenient = true } + private val _state = MutableStateFlow(State.Loading) + val state = _state.asStateFlow() + fun tryLoad() { viewModelScope.launch { - val remotes = loadIrFileContent() - val pagesLayoutResult = pagesRepository.fetchDefaultPageLayout( - ifrFileId = param.ifrFileId, - ) - pagesLayoutResult - .onFailure { _state.emit(State.Error) } - .onFailure { throwable -> error(throwable) { "#tryLoad could not load ui model" } } - .onSuccess { pagesLayout -> - _state.emit( - State.Loaded( - pagesLayout = pagesLayout, - remotes = remotes.toImmutableList() - ) - ) + 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))) + } + .onFailure { _state.emit(State.Error) } + .onFailure { throwable -> error(throwable) { "#tryLoad could not load ui model" } } + .getOrNull() ?: return@launch + + 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() + _state.emit( + value = State.Loaded( + pagesLayout = pagesLayout, + remotes = remotesRaw + .let(FlipperFileFormat::fromFileContent) + .let(InfraredKeyParser::mapParsedKeyToInfraredRemotes) + .toImmutableList(), + remotesRaw = remotesRaw, + isDownloadedOnFlipper = localPagesLayout != null && localRemotesRaw != null + ) + ) } } @@ -67,7 +97,9 @@ class GridViewModel @AssistedInject constructor( data object Error : State data class Loaded( val pagesLayout: PagesLayout, - val remotes: ImmutableList + val remotes: ImmutableList, + val remotesRaw: String, + val isDownloadedOnFlipper: Boolean ) : State } @@ -75,7 +107,12 @@ class GridViewModel @AssistedInject constructor( fun interface Factory { operator fun invoke( param: GridScreenDecomposeComponent.Param, - onIrFileLoaded: (String) -> Unit + onCallback: (Callback) -> Unit, ): GridViewModel } + + sealed interface Callback { + data class InfraredFileLoaded(val content: String) : Callback + data class UiLoaded(val content: String) : Callback + } } diff --git a/components/remote-controls/main/api/build.gradle.kts b/components/remote-controls/main/api/build.gradle.kts index dd8cb18ad5..d165ee1fad 100644 --- a/components/remote-controls/main/api/build.gradle.kts +++ b/components/remote-controls/main/api/build.gradle.kts @@ -5,8 +5,6 @@ plugins { android.namespace = "com.flipperdevices.remotecontrols.device.select.api" dependencies { - implementation(projects.components.deeplink.api) - implementation(projects.components.core.ui.decompose) implementation(libs.compose.ui) diff --git a/components/remote-controls/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/RemoteControlsScreenDecomposeComponent.kt b/components/remote-controls/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/RemoteControlsScreenDecomposeComponent.kt index 65b82d735e..2c96ccbd84 100644 --- a/components/remote-controls/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/RemoteControlsScreenDecomposeComponent.kt +++ b/components/remote-controls/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/RemoteControlsScreenDecomposeComponent.kt @@ -8,7 +8,7 @@ abstract class RemoteControlsScreenDecomposeComponent : CompositeDecomp fun interface Factory { operator fun invoke( componentContext: ComponentContext, - onBack: DecomposeOnBackParameter + onBack: DecomposeOnBackParameter, ): RemoteControlsScreenDecomposeComponent<*> } } diff --git a/components/remote-controls/main/impl/build.gradle.kts b/components/remote-controls/main/impl/build.gradle.kts index 75484290ca..3895c2322c 100644 --- a/components/remote-controls/main/impl/build.gradle.kts +++ b/components/remote-controls/main/impl/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { implementation(projects.components.core.ui.res) implementation(projects.components.bridge.dao.api) - implementation(projects.components.deeplink.api) implementation(projects.components.bridge.service.api) implementation(projects.components.bridge.pbutils) implementation(projects.components.bridge.api) diff --git a/components/remote-controls/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/api/RemoteControlsScreenDecomposeComponentImpl.kt b/components/remote-controls/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/api/RemoteControlsScreenDecomposeComponentImpl.kt index 669d4af79a..0c86f26799 100644 --- a/components/remote-controls/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/api/RemoteControlsScreenDecomposeComponentImpl.kt +++ b/components/remote-controls/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/api/RemoteControlsScreenDecomposeComponentImpl.kt @@ -4,11 +4,9 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.pushToFront -import com.arkivanov.decompose.router.stack.replaceCurrent import com.flipperdevices.core.di.AppGraph import com.flipperdevices.remotecontrols.api.BrandsScreenDecomposeComponent import com.flipperdevices.remotecontrols.api.CategoriesScreenDecomposeComponent -import com.flipperdevices.remotecontrols.api.GridScreenDecomposeComponent import com.flipperdevices.remotecontrols.api.RemoteControlsScreenDecomposeComponent import com.flipperdevices.remotecontrols.api.SetupScreenDecomposeComponent import com.flipperdevices.remotecontrols.impl.api.model.RemoteControlsNavigationConfig @@ -18,6 +16,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import me.gulya.anvil.assisted.ContributesAssistedFactory +@Suppress("LongParameterList") @ContributesAssistedFactory(AppGraph::class, RemoteControlsScreenDecomposeComponent.Factory::class) class RemoteControlsScreenDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @@ -25,7 +24,6 @@ class RemoteControlsScreenDecomposeComponentImpl @AssistedInject constructor( private val categoriesScreenDecomposeComponentFactory: CategoriesScreenDecomposeComponent.Factory, private val brandsScreenDecomposeComponentFactory: BrandsScreenDecomposeComponent.Factory, private val setupScreenDecomposeComponentFactory: SetupScreenDecomposeComponent.Factory, - private val gridScreenDecomposeComponentFactory: GridScreenDecomposeComponent.Factory ) : RemoteControlsScreenDecomposeComponent(), ComponentContext by componentContext { @@ -42,59 +40,41 @@ class RemoteControlsScreenDecomposeComponentImpl @AssistedInject constructor( componentContext: ComponentContext ): DecomposeComponent = when (config) { is RemoteControlsNavigationConfig.SelectCategory -> { - categoriesScreenDecomposeComponentFactory - .invoke( - componentContext = componentContext, - onBackClick = onBack::invoke, - onCategoryClick = { deviceCategoryId -> - val configuration = RemoteControlsNavigationConfig.Brands(deviceCategoryId) - navigation.pushToFront(configuration) - } - ) + categoriesScreenDecomposeComponentFactory( + componentContext = componentContext, + onBackClick = onBack::invoke, + onCategoryClick = { deviceCategoryId -> + val configuration = RemoteControlsNavigationConfig.Brands(deviceCategoryId) + navigation.pushToFront(configuration) + } + ) } is RemoteControlsNavigationConfig.Brands -> { - brandsScreenDecomposeComponentFactory - .createBrandsComponent( - componentContext = componentContext, - onBackClick = navigation::pop, - categoryId = config.categoryId, - onBrandClick = { brandId -> - val configuration = RemoteControlsNavigationConfig.Setup( - categoryId = config.categoryId, - brandId = brandId - ) - navigation.pushToFront(configuration) - } - ) + brandsScreenDecomposeComponentFactory( + componentContext = componentContext, + onBackClick = navigation::pop, + categoryId = config.categoryId, + onBrandClick = { brandId -> + val configuration = RemoteControlsNavigationConfig.Setup( + categoryId = config.categoryId, + brandId = brandId + ) + navigation.pushToFront(configuration) + } + ) } is RemoteControlsNavigationConfig.Setup -> { - setupScreenDecomposeComponentFactory - .invoke( - componentContext = componentContext, - param = SetupScreenDecomposeComponent.Param( - brandId = config.brandId, - categoryId = config.categoryId - ), - onBack = navigation::pop, - onIfrFileFound = { - val configuration = RemoteControlsNavigationConfig.Grid(ifrFileId = it) - navigation.replaceCurrent(configuration) - } - ) - } - - is RemoteControlsNavigationConfig.Grid -> { - gridScreenDecomposeComponentFactory - .invoke( - componentContext = componentContext, - param = GridScreenDecomposeComponent.Param( - ifrFileId = config.ifrFileId, - uiFileId = null, - ), - onPopClick = navigation::pop - ) + setupScreenDecomposeComponentFactory( + componentContext = componentContext, + param = SetupScreenDecomposeComponent.Param( + brandId = config.brandId, + categoryId = config.categoryId + ), + onBack = navigation::pop, + onIrFileReady = { navigation.pop() } + ) } } } diff --git a/components/remote-controls/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/api/model/RemoteControlsNavigationConfig.kt b/components/remote-controls/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/api/model/RemoteControlsNavigationConfig.kt index d20059604b..1d18b68f8b 100644 --- a/components/remote-controls/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/api/model/RemoteControlsNavigationConfig.kt +++ b/components/remote-controls/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/api/model/RemoteControlsNavigationConfig.kt @@ -14,7 +14,4 @@ sealed interface RemoteControlsNavigationConfig { @Serializable class Setup(val categoryId: Long, val brandId: Long) : RemoteControlsNavigationConfig - - @Serializable - class Grid(val ifrFileId: Long) : RemoteControlsNavigationConfig } diff --git a/components/remote-controls/setup/api/build.gradle.kts b/components/remote-controls/setup/api/build.gradle.kts index a06d719fc6..e9bdc75052 100644 --- a/components/remote-controls/setup/api/build.gradle.kts +++ b/components/remote-controls/setup/api/build.gradle.kts @@ -5,8 +5,6 @@ plugins { android.namespace = "com.flipperdevices.remotecontrols.setup.api" dependencies { - implementation(projects.components.deeplink.api) - implementation(projects.components.core.ui.decompose) implementation(projects.components.infrared.utils) implementation(projects.components.bridge.dao.api) diff --git a/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/DispatchSignalApi.kt b/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/DispatchSignalApi.kt index 6c6620449c..bdf69dcf2c 100644 --- a/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/DispatchSignalApi.kt +++ b/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/DispatchSignalApi.kt @@ -16,7 +16,7 @@ interface DispatchSignalApi : InstanceKeeper.Instance { /** * Dispatch key from temporal file which contains only one key */ - fun dispatch(config: EmulateConfig) + fun dispatch(config: EmulateConfig, identifier: IfrKeyIdentifier) fun reset() @@ -32,7 +32,7 @@ interface DispatchSignalApi : InstanceKeeper.Instance { sealed interface State { data object Pending : State data object FlipperIsBusy : State - data object Emulating : State + data class Emulating(val ifrKeyIdentifier: IfrKeyIdentifier) : State data object Error : State } } 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 206ab52684..ff3335a4b8 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 @@ -1,14 +1,17 @@ package com.flipperdevices.remotecontrols.api import com.arkivanov.essenty.instancekeeper.InstanceKeeper -import com.flipperdevices.bridge.dao.api.model.FlipperFileFormat import kotlinx.coroutines.flow.StateFlow interface SaveTempSignalApi : InstanceKeeper.Instance { val state: StateFlow - fun saveTempFile(fff: FlipperFileFormat, nameWithExtension: String) + fun saveFile( + textContent: String, + nameWithExtension: String, + extFolderPath: String + ) sealed interface State { data object Pending : State diff --git a/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/SetupScreenDecomposeComponent.kt b/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/SetupScreenDecomposeComponent.kt index 2122193efc..f6eb6c6730 100644 --- a/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/SetupScreenDecomposeComponent.kt +++ b/components/remote-controls/setup/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/SetupScreenDecomposeComponent.kt @@ -8,11 +8,11 @@ abstract class SetupScreenDecomposeComponent( ) : ScreenDecomposeComponent(componentContext) { interface Factory { - fun invoke( + operator fun invoke( componentContext: ComponentContext, param: Param, onBack: () -> Unit, - onIfrFileFound: (ifrFileId: Long) -> Unit + onIrFileReady: (id: Long) -> Unit ): SetupScreenDecomposeComponent } diff --git a/components/remote-controls/setup/impl/build.gradle.kts b/components/remote-controls/setup/impl/build.gradle.kts index f821be1bd4..7ea361ce66 100644 --- a/components/remote-controls/setup/impl/build.gradle.kts +++ b/components/remote-controls/setup/impl/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { implementation(projects.components.core.ui.dialog) implementation(projects.components.bridge.dao.api) - implementation(projects.components.deeplink.api) implementation(projects.components.bridge.service.api) implementation(projects.components.bridge.pbutils) implementation(projects.components.bridge.api) diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/api/save/file/SaveFileApi.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/api/save/file/SaveFileApi.kt index 99a9bab437..41d17033a1 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/api/save/file/SaveFileApi.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/api/save/file/SaveFileApi.kt @@ -1,13 +1,12 @@ package com.flipperdevices.remotecontrols.impl.setup.api.save.file import com.flipperdevices.bridge.api.manager.FlipperRequestApi -import com.flipperdevices.deeplink.model.DeeplinkContent import kotlinx.coroutines.flow.Flow interface SaveFileApi { fun save( requestApi: FlipperRequestApi, - deeplinkContent: DeeplinkContent, + textContent: String, absolutePath: String ): Flow diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/api/save/file/SaveFileApiImpl.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/api/save/file/SaveFileApiImpl.kt index 64e71e7015..f3db912f35 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/api/save/file/SaveFileApiImpl.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/api/save/file/SaveFileApiImpl.kt @@ -1,6 +1,5 @@ package com.flipperdevices.remotecontrols.impl.setup.api.save.file -import android.content.Context import com.flipperdevices.bridge.api.manager.FlipperRequestApi import com.flipperdevices.bridge.api.model.FlipperRequest import com.flipperdevices.bridge.api.model.FlipperRequestPriority @@ -9,7 +8,6 @@ import com.flipperdevices.bridge.protobuf.streamToCommandFlow import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.info -import com.flipperdevices.deeplink.model.DeeplinkContent import com.flipperdevices.protobuf.main import com.flipperdevices.protobuf.storage.file import com.flipperdevices.protobuf.storage.writeRequest @@ -18,23 +16,22 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map +import java.io.ByteArrayInputStream import javax.inject.Inject @ContributesBinding(AppGraph::class, SaveFileApi::class) -class SaveFileApiImpl @Inject constructor( - private val context: Context -) : LogTagProvider, SaveFileApi { +class SaveFileApiImpl @Inject constructor() : LogTagProvider, SaveFileApi { override val TAG: String = "SaveFileApi" override fun save( requestApi: FlipperRequestApi, - deeplinkContent: DeeplinkContent, + textContent: String, absolutePath: String ): Flow = channelFlow { - val totalSize = deeplinkContent.length() ?: 0 + val byteArray = textContent.toByteArray() + val totalSize = byteArray.size.toLong() var progressInternal = 0L - val contentResolver = context.contentResolver - deeplinkContent.openStream(contentResolver).use { fileStream -> - val stream = fileStream ?: return@use + info { "#save Saving content: size: $totalSize $textContent" } + ByteArrayInputStream(byteArray).use { stream -> val commandFlow = streamToCommandFlow( stream, totalSize, 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 bdd7450d6b..4ba4865013 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 @@ -45,9 +45,11 @@ fun SetupScreen( setupComponent.model(coroutineScope) }.collectAsState() LaunchedEffect(setupComponent.remoteFoundFlow) { - setupComponent.remoteFoundFlow - .onEach { setupComponent.onFileFound(it) } - .launchIn(this) + setupComponent.remoteFoundFlow.onEach { + setupComponent.onFileFound(it) + val configuration = RootScreenConfig.RemoteControlGrid.Id(it.id) + rootNavigation.push(configuration) + }.launchIn(this) } Scaffold( modifier = modifier, 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 368c57f58c..c571d7e6fd 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 @@ -19,6 +19,7 @@ import com.flipperdevices.core.ui.theme.FlipperThemeInternal import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.core.ui.theme.LocalTypography import com.flipperdevices.ifrmvp.core.ui.button.ButtonItemComposable +import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.ifrmvp.model.buttondata.ButtonData import com.flipperdevices.ifrmvp.model.buttondata.TextButtonData import com.flipperdevices.remotecontrols.setup.impl.R as SetupR @@ -27,11 +28,13 @@ import com.flipperdevices.remotecontrols.setup.impl.R as SetupR private fun SignalResponseButton( data: ButtonData, onClick: () -> Unit, + emulatedKeyIdentifier: IfrKeyIdentifier? ) { ButtonItemComposable( buttonData = data, onKeyDataClick = { onClick.invoke() }, modifier = Modifier.size(64.dp), + emulatedKeyIdentifier = emulatedKeyIdentifier, ) } @@ -39,6 +42,7 @@ private fun SignalResponseButton( fun ButtonContent( onClick: () -> Unit, data: ButtonData, + emulatedKeyIdentifier: IfrKeyIdentifier?, categoryName: String, modifier: Modifier = Modifier, ) { @@ -50,6 +54,7 @@ fun ButtonContent( SignalResponseButton( data = data, onClick = onClick, + emulatedKeyIdentifier = emulatedKeyIdentifier, ) Spacer(modifier = Modifier.height(14.dp)) Text( @@ -74,17 +79,20 @@ private fun ComposableConfirmContentDarkPreview() { ButtonContent( onClick = {}, categoryName = "CATEGORY", - data = TextButtonData(text = "Hello") + data = TextButtonData(text = "Hello"), + emulatedKeyIdentifier = null ) ButtonContent( onClick = {}, categoryName = "CATEGORY 2", - data = TextButtonData(text = "TV/AV") + data = TextButtonData(text = "TV/AV"), + emulatedKeyIdentifier = null ) ButtonContent( onClick = {}, categoryName = "CATEGORY 2", - data = TextButtonData(text = "Hello world") + data = TextButtonData(text = "Hello world"), + emulatedKeyIdentifier = null ) } } 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 eab49bee10..56d4d8d449 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 @@ -7,6 +7,7 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -38,6 +39,7 @@ fun LoadedContent( modifier = Modifier.align(Alignment.Center), data = signalResponse.data, categoryName = signalResponse.categoryName, + emulatedKeyIdentifier = model.emulatedKeyIdentifier ) AnimatedVisibility( visible = model.isEmulated, @@ -45,7 +47,8 @@ fun LoadedContent( exit = slideOutVertically(), modifier = Modifier .fillMaxWidth() - .align(Alignment.BottomCenter), + .align(Alignment.BottomCenter) + .navigationBarsPadding(), ) { ConfirmContent( text = signalResponse.message.format(signalResponse.categoryName), @@ -76,7 +79,8 @@ private fun LoadedContentPreview() { LoadedContent( model = SetupComponent.Model.Loaded( response = SignalResponseModel(), - isEmulated = true + isEmulated = true, + emulatedKeyIdentifier = null ), onPositiveClick = {}, onNegativeClick = {}, 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 114e4331c0..d265346c8e 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 @@ -3,6 +3,7 @@ package com.flipperdevices.remotecontrols.impl.setup.presentation.decompose import com.arkivanov.decompose.ComponentContext import com.flipperdevices.ifrmvp.backend.model.IfrFileModel import com.flipperdevices.ifrmvp.backend.model.SignalResponseModel +import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.remotecontrols.api.SetupScreenDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import kotlinx.coroutines.CoroutineScope @@ -32,7 +33,7 @@ interface SetupComponent { data class Loaded( val response: SignalResponseModel, val isFlipperBusy: Boolean = false, - val isEmulating: Boolean = false, + val emulatedKeyIdentifier: IfrKeyIdentifier?, val isEmulated: Boolean ) : Model @@ -44,7 +45,7 @@ interface SetupComponent { componentContext: ComponentContext, param: SetupScreenDecomposeComponent.Param, onBack: DecomposeOnBackParameter, - onIfrFileFound: (ifrFileId: Long) -> Unit + onIrFileReady: (id: Long) -> Unit ): SetupComponent } } 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 34623395c0..8a1e7600a0 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 @@ -7,6 +7,8 @@ import com.flipperdevices.bridge.dao.api.model.FlipperKeyType import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ktx.jre.FlipperDispatchers import com.flipperdevices.ifrmvp.backend.model.IfrFileModel +import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier +import com.flipperdevices.ifrmvp.model.buttondata.SingleKeyButtonData import com.flipperdevices.keyemulate.model.EmulateConfig import com.flipperdevices.remotecontrols.api.DispatchSignalApi import com.flipperdevices.remotecontrols.api.SaveTempSignalApi @@ -35,7 +37,7 @@ class SetupComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @Assisted override val param: SetupScreenDecomposeComponent.Param, @Assisted private val onBackClick: DecomposeOnBackParameter, - @Assisted private val onIfrFileFound: (ifrFileId: Long) -> Unit, + @Assisted private val onIfrFileFound: (id: Long) -> Unit, currentSignalViewModelFactory: CurrentSignalViewModel.Factory, createHistoryViewModel: Provider, createSaveTempSignalApi: Provider, @@ -64,9 +66,10 @@ class SetupComponentImpl @AssistedInject constructor( factory = { currentSignalViewModelFactory.invoke(param) { responseModel -> val signalModel = responseModel.signalResponse?.signalModel ?: return@invoke - saveSignalApi.saveTempFile( - fff = signalModel.toFFFormat(), - nameWithExtension = TEMP_FILE_NAME + saveSignalApi.saveFile( + textContent = signalModel.toFFFormat().openStream().reader().readText(), + nameWithExtension = TEMP_FILE_NAME, + extFolderPath = ABSOLUTE_TEMP_FOLDER_PATH ) } } @@ -77,6 +80,7 @@ class SetupComponentImpl @AssistedInject constructor( dispatchSignalApi.state, dispatchSignalApi.isEmulated, transform = { signalState, saveState, dispatchState, isEmulated -> + val emulatingState = (dispatchState as? DispatchSignalApi.State.Emulating) when (signalState) { CurrentSignalViewModel.State.Error -> SetupComponent.Model.Error is CurrentSignalViewModel.State.Loaded -> { @@ -85,14 +89,14 @@ class SetupComponentImpl @AssistedInject constructor( SaveTempSignalApi.State.Pending -> SetupComponent.Model.Loaded( response = signalState.response, isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, - isEmulating = dispatchState is DispatchSignalApi.State.Emulating, + emulatedKeyIdentifier = emulatingState?.ifrKeyIdentifier, isEmulated = isEmulated ) SaveTempSignalApi.State.Uploaded -> SetupComponent.Model.Loaded( response = signalState.response, isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, - isEmulating = dispatchState is DispatchSignalApi.State.Emulating, + emulatedKeyIdentifier = emulatingState?.ifrKeyIdentifier, isEmulated = isEmulated ) @@ -153,18 +157,21 @@ class SetupComponentImpl @AssistedInject constructor( val signalModel = loadedState.response.signalResponse?.signalModel ?: return val config = EmulateConfig( keyPath = FlipperFilePath( - FLIPPER_TEMP_FOLDER, + ABSOLUTE_TEMP_FOLDER_PATH, TEMP_FILE_NAME ), keyType = FlipperKeyType.INFRARED, args = signalModel.remote.name, index = 0 ) - dispatchSignalApi.dispatch(config) + val keyIdentifier = (loadedState.response.signalResponse?.data as? SingleKeyButtonData) + ?.keyIdentifier + ?: IfrKeyIdentifier.Unknown + dispatchSignalApi.dispatch(config, keyIdentifier) } override fun onBackClick() = onBackClick.invoke() } -private val FLIPPER_TEMP_FOLDER = FlipperKeyType.INFRARED.flipperDir + "/temp" +private val ABSOLUTE_TEMP_FOLDER_PATH = "/${FlipperKeyType.INFRARED.flipperDir}/temp" private const val TEMP_FILE_NAME = "temp.ir" diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupScreenDecomposeComponentImpl.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupScreenDecomposeComponentImpl.kt index da750758fc..9ac4b68efb 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupScreenDecomposeComponentImpl.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupScreenDecomposeComponentImpl.kt @@ -16,14 +16,14 @@ class SetupScreenDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @Assisted param: SetupScreenDecomposeComponent.Param, @Assisted onBack: () -> Unit, - @Assisted onIfrFileFound: (ifrFileId: Long) -> Unit, + @Assisted onIrFileReady: (id: Long) -> Unit, setupComponentFactory: SetupComponent.Factory, ) : SetupScreenDecomposeComponent(componentContext) { private val setupComponent = setupComponentFactory.createSetupComponent( componentContext = childContext("SetupComponent"), param = param, onBack = onBack, - onIfrFileFound = onIfrFileFound + onIrFileReady = onIrFileReady ) @Composable diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/DispatchSignalViewModel.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/DispatchSignalViewModel.kt index 643dd8ebe8..a7a5133bf6 100644 --- a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/DispatchSignalViewModel.kt +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/DispatchSignalViewModel.kt @@ -93,14 +93,14 @@ class DispatchSignalViewModel @Inject constructor( args = remote.name, index = i ) - dispatch(config) + dispatch(config, identifier) } override fun dismissBusyDialog() { _state.value = DispatchSignalApi.State.Pending } - override fun dispatch(config: EmulateConfig) { + override fun dispatch(config: EmulateConfig, identifier: IfrKeyIdentifier) { if (latestDispatchJob?.isActive == true) return latestDispatchJob = viewModelScope.launch(Dispatchers.Main) { _state.emit(DispatchSignalApi.State.Pending) @@ -109,7 +109,7 @@ class DispatchSignalViewModel @Inject constructor( onError = { _state.value = DispatchSignalApi.State.Error }, onBleManager = { serviceApi -> launch { - _state.emit(DispatchSignalApi.State.Emulating) + _state.emit(DispatchSignalApi.State.Emulating(identifier)) try { emulateHelper.startEmulate( scope = this, 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 bff384a070..184160d500 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,9 +1,6 @@ package com.flipperdevices.remotecontrols.impl.setup.presentation.viewmodel -import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope -import com.flipperdevices.bridge.dao.api.model.FlipperFileFormat import com.flipperdevices.bridge.dao.api.model.FlipperFilePath -import com.flipperdevices.bridge.dao.api.model.FlipperKeyType import com.flipperdevices.bridge.service.api.FlipperServiceApi import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceConsumer import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider @@ -12,24 +9,20 @@ import com.flipperdevices.core.ktx.jre.FlipperDispatchers import com.flipperdevices.core.ktx.jre.launchWithLock import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel -import com.flipperdevices.deeplink.model.DeeplinkContent import com.flipperdevices.remotecontrols.api.SaveTempSignalApi import com.flipperdevices.remotecontrols.impl.setup.api.save.file.SaveFileApi import com.flipperdevices.remotecontrols.impl.setup.api.save.folder.SaveFolderApi import com.squareup.anvil.annotations.ContributesBinding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.withContext import javax.inject.Inject -private val EXT_IFR_TEMP_FOLDER = "/ext/${FlipperKeyType.INFRARED.flipperDir}/temp" -private val IFR_TEMP_FOLDER = FlipperKeyType.INFRARED.flipperDir + "/temp" +private const val EXT_PATH = "/ext" @ContributesBinding(AppGraph::class, SaveTempSignalApi::class) class SaveTempSignalViewModel @Inject constructor( @@ -45,46 +38,49 @@ class SaveTempSignalViewModel @Inject constructor( private val _state = MutableStateFlow(SaveTempSignalApi.State.Pending) override val state = _state.asStateFlow() - override fun saveTempFile(fff: FlipperFileFormat, nameWithExtension: String) { - _state.value = SaveTempSignalApi.State.Uploading(0, 0) - launchWithLock(mutex, viewModelScope, "load") { - val serviceApi = withContext(Dispatchers.Main) { serviceProvider.getServiceApi() } - saveFolderApi.save(serviceApi.requestApi, EXT_IFR_TEMP_FOLDER) - save( - serviceApi = serviceApi, - fff = fff, - ffPath = FlipperFilePath( - folder = IFR_TEMP_FOLDER, - nameWithExtension = nameWithExtension - ) - ) - } - } + override fun saveFile( + textContent: String, + nameWithExtension: String, + extFolderPath: String + ) = save( + extFolderPath = extFolderPath, + textContent = textContent, + absolutePath = FlipperFilePath( + folder = extFolderPath, + nameWithExtension = nameWithExtension + ).getPathOnFlipper(), + ) - private suspend fun save( - serviceApi: FlipperServiceApi, - fff: FlipperFileFormat, - ffPath: FlipperFilePath - ) = coroutineScope { - val deeplinkContent = DeeplinkContent.FFFContent(ffPath.nameWithExtension, fff) - val saveFileFlow = saveFileApi.save( - requestApi = serviceApi.requestApi, - deeplinkContent = deeplinkContent, - absolutePath = ffPath.getPathOnFlipper() - ) - 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 - ) - } + private fun save( + textContent: String, + absolutePath: String, + extFolderPath: String + ) { + 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 + ) + } + } + .collect() + _state.value = SaveTempSignalApi.State.Uploaded } - .collect() - _state.value = SaveTempSignalApi.State.Uploaded + } } 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 cd8bb02d64..6a5f8c10e9 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 @@ -37,4 +37,10 @@ sealed class RootScreenConfig { @Serializable data object RemoteControls : RootScreenConfig() + + @Serializable + sealed class RemoteControlGrid : RootScreenConfig() { + data class Id(val ifrFileId: Long) : RemoteControlGrid() + data class Path(val flipperKeyPath: FlipperKeyPath) : RemoteControlGrid() + } } diff --git a/components/rootscreen/impl/build.gradle.kts b/components/rootscreen/impl/build.gradle.kts index b65e2dbc1f..198ac3a620 100644 --- a/components/rootscreen/impl/build.gradle.kts +++ b/components/rootscreen/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.components.bridge.dao.api) implementation(projects.components.changelog.api) + implementation(projects.components.remoteControls.grid.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 24eaafacf2..2676eed79e 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,6 +10,7 @@ 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 @@ -22,6 +23,7 @@ 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.RemoteControlsScreenDecomposeComponent import com.flipperdevices.rootscreen.api.RootDecomposeComponent import com.flipperdevices.rootscreen.impl.deeplink.RootDeeplinkHandler @@ -57,7 +59,8 @@ class RootDecomposeComponentImpl @AssistedInject constructor( private val keyScreenFactory: KeyScreenDecomposeComponent.Factory, private val screenshotsPreviewFactory: ScreenshotsPreviewDecomposeComponent.Factory, private val changelogScreenDecomposeFactory: ChangelogScreenDecomposeComponent.Factory, - private val remoteControlsComponentFactory: RemoteControlsScreenDecomposeComponent.Factory + private val remoteControlsComponentFactory: RemoteControlsScreenDecomposeComponent.Factory, + private val gridScreenDecomposeComponentFactory: GridScreenDecomposeComponent.Factory ) : RootDecomposeComponent, ComponentContext by componentContext { private val scope = coroutineScope(FlipperDispatchers.workStealingDispatcher) private val navigation = StackNavigation() @@ -71,6 +74,7 @@ class RootDecomposeComponentImpl @AssistedInject constructor( ) private val deeplinkHandler = RootDeeplinkHandler(navigation, stack, firstPairApi) + @Suppress("LongMethod") private fun child( config: RootScreenConfig, componentContext: ComponentContext @@ -129,9 +133,22 @@ class RootDecomposeComponentImpl @AssistedInject constructor( updateRequest = config.updateRequest, onBack = this::internalOnBack ) - RootScreenConfig.RemoteControls -> remoteControlsComponentFactory( + + is RootScreenConfig.RemoteControls -> remoteControlsComponentFactory( + componentContext = componentContext, + onBack = this::internalOnBack + ) + + is RootScreenConfig.RemoteControlGrid.Id -> gridScreenDecomposeComponentFactory( + componentContext = componentContext, + param = GridScreenDecomposeComponent.Param.Id(config.ifrFileId), + onPopClick = navigation::pop + ) + + is RootScreenConfig.RemoteControlGrid.Path -> gridScreenDecomposeComponentFactory( componentContext = componentContext, - onBack = { navigation.popOr(onBack::invoke) } + param = GridScreenDecomposeComponent.Param.Path(config.flipperKeyPath), + onPopClick = navigation::pop ) }