diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0ec98b32..ef7837d2cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Attention: don't forget to add the flag for F-Droid before release - [FIX] Use kotlin 2.0.20-RC2 for bypass Options screen crash - [FIX] Relocate remote-controls button to infrared remotes screen - [FIX] Fap manifest caching +- [FIX] Remote controls design issues - [CI] Fix merge-queue files diff - [CI] Add https://github.com/LionZXY/detekt-decompose-rule - [CI] Enabling detekt module for android and kmp modules diff --git a/components/infrared/api/build.gradle.kts b/components/infrared/api/build.gradle.kts index 6f5ee59a60..70827a8495 100644 --- a/components/infrared/api/build.gradle.kts +++ b/components/infrared/api/build.gradle.kts @@ -7,6 +7,9 @@ android.namespace = "com.flipperdevices.infrared.api" dependencies { implementation(projects.components.core.ui.decompose) implementation(projects.components.bridge.dao.api) + implementation(projects.components.bridge.service.api) + + implementation(libs.kotlin.coroutines) implementation(libs.decompose) } diff --git a/components/infrared/api/src/main/kotlin/com/flipperdevices/infrared/api/InfraredConnectionApi.kt b/components/infrared/api/src/main/kotlin/com/flipperdevices/infrared/api/InfraredConnectionApi.kt new file mode 100644 index 0000000000..71308ac937 --- /dev/null +++ b/components/infrared/api/src/main/kotlin/com/flipperdevices/infrared/api/InfraredConnectionApi.kt @@ -0,0 +1,12 @@ +package com.flipperdevices.infrared.api + +import com.flipperdevices.bridge.service.api.FlipperServiceApi +import kotlinx.coroutines.flow.Flow + +interface InfraredConnectionApi { + fun getState(serviceApi: FlipperServiceApi): Flow + + enum class InfraredEmulateState { + NOT_CONNECTED, CONNECTING, SYNCING, UPDATE_FLIPPER, ALL_GOOD + } +} diff --git a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/api/InfraredConnectionApiImpl.kt b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/api/InfraredConnectionApiImpl.kt new file mode 100644 index 0000000000..512e6236bf --- /dev/null +++ b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/api/InfraredConnectionApiImpl.kt @@ -0,0 +1,55 @@ +package com.flipperdevices.infrared.impl.api + +import com.flipperdevices.bridge.api.manager.ktx.state.ConnectionState +import com.flipperdevices.bridge.api.manager.ktx.state.FlipperSupportedState +import com.flipperdevices.bridge.api.utils.Constants.API_SUPPORTED_INFRARED_EMULATE +import com.flipperdevices.bridge.service.api.FlipperServiceApi +import com.flipperdevices.bridge.synchronization.api.SynchronizationApi +import com.flipperdevices.bridge.synchronization.api.SynchronizationState +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.log.info +import com.flipperdevices.infrared.api.InfraredConnectionApi +import com.squareup.anvil.annotations.ContributesBinding +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + +@ContributesBinding(AppGraph::class, InfraredConnectionApi::class) +class InfraredConnectionApiImpl @Inject constructor( + private val synchronizationApi: SynchronizationApi +) : InfraredConnectionApi { + override fun getState( + serviceApi: FlipperServiceApi + ): Flow { + return combine( + serviceApi.flipperVersionApi.getVersionInformationFlow(), + serviceApi.connectionInformationApi.getConnectionStateFlow(), + synchronizationApi.getSynchronizationState() + ) { versionInformation, connectionState, synchronizationState -> + val infraredEmulateState = when { + connectionState is ConnectionState.Disconnected -> { + InfraredConnectionApi.InfraredEmulateState.NOT_CONNECTED + } + + connectionState !is ConnectionState.Ready || + connectionState.supportedState != FlipperSupportedState.READY -> { + InfraredConnectionApi.InfraredEmulateState.CONNECTING + } + + synchronizationState is SynchronizationState.InProgress -> { + InfraredConnectionApi.InfraredEmulateState.SYNCING + } + + versionInformation == null || versionInformation < API_SUPPORTED_INFRARED_EMULATE -> { + InfraredConnectionApi.InfraredEmulateState.UPDATE_FLIPPER + } + + else -> { + InfraredConnectionApi.InfraredEmulateState.ALL_GOOD + } + } + info { "#onServiceApiReady $infraredEmulateState" } + infraredEmulateState + } + } +} diff --git a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/composable/ComposableInfraredScreen.kt b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/composable/ComposableInfraredScreen.kt index c9d1cc5c12..a8437256fc 100644 --- a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/composable/ComposableInfraredScreen.kt +++ b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/composable/ComposableInfraredScreen.kt @@ -13,10 +13,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.arkivanov.decompose.ComponentContext import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath +import com.flipperdevices.infrared.api.InfraredConnectionApi.InfraredEmulateState import com.flipperdevices.infrared.impl.R import com.flipperdevices.infrared.impl.composable.components.ComposableIconText import com.flipperdevices.infrared.impl.composable.components.ComposableInfraredAppBar -import com.flipperdevices.infrared.impl.viewmodel.InfraredEmulateState import com.flipperdevices.infrared.impl.viewmodel.InfraredViewModel import com.flipperdevices.keyemulate.api.KeyEmulateApi import com.flipperdevices.keyemulate.api.KeyEmulateUiApi diff --git a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/composable/ComposableInfraredScreenReady.kt b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/composable/ComposableInfraredScreenReady.kt index 710b7b8896..732cbfee40 100644 --- a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/composable/ComposableInfraredScreenReady.kt +++ b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/composable/ComposableInfraredScreenReady.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.flipperdevices.infrared.api.InfraredConnectionApi.InfraredEmulateState import com.flipperdevices.infrared.impl.composable.components.ComposableFadedInfraredRemotes import com.flipperdevices.infrared.impl.composable.components.ComposableInfraredRemotes -import com.flipperdevices.infrared.impl.viewmodel.InfraredEmulateState import com.flipperdevices.keyemulate.model.EmulateConfig import com.flipperdevices.keyscreen.model.KeyScreenState diff --git a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/viewmodel/InfraredViewModel.kt b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/viewmodel/InfraredViewModel.kt index b5426a0639..9d9f2268ec 100644 --- a/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/viewmodel/InfraredViewModel.kt +++ b/components/infrared/impl/src/main/kotlin/com/flipperdevices/infrared/impl/viewmodel/InfraredViewModel.kt @@ -1,32 +1,28 @@ package com.flipperdevices.infrared.impl.viewmodel -import com.flipperdevices.bridge.api.manager.ktx.state.ConnectionState -import com.flipperdevices.bridge.api.manager.ktx.state.FlipperSupportedState -import com.flipperdevices.bridge.api.utils.Constants.API_SUPPORTED_INFRARED_EMULATE import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath import com.flipperdevices.bridge.service.api.FlipperServiceApi import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceConsumer import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider -import com.flipperdevices.bridge.synchronization.api.SynchronizationApi -import com.flipperdevices.bridge.synchronization.api.SynchronizationState import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.info import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.infrared.api.InfraredConnectionApi +import com.flipperdevices.infrared.api.InfraredConnectionApi.InfraredEmulateState import com.flipperdevices.keyscreen.api.KeyStateHelperApi import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach class InfraredViewModel @AssistedInject constructor( @Assisted val keyPath: FlipperKeyPath, // For get value to bottom sheet keyStateHelperApi: KeyStateHelperApi.Builder, - private val synchronizationApi: SynchronizationApi, - serviceProvider: FlipperServiceProvider + serviceProvider: FlipperServiceProvider, + private val infraredConnectionApi: InfraredConnectionApi ) : DecomposeViewModel(), FlipperBleServiceConsumer, LogTagProvider { override val TAG: String = "InfraredViewModel" @@ -45,31 +41,12 @@ class InfraredViewModel @AssistedInject constructor( fun onRename(onEndAction: (FlipperKeyPath) -> Unit) = keyStateHelper.onOpenEdit(onEndAction) fun onDelete(onEndAction: () -> Unit) = keyStateHelper.onDelete(onEndAction) + override fun onServiceApiReady(serviceApi: FlipperServiceApi) { - combine( - serviceApi.flipperVersionApi.getVersionInformationFlow(), - serviceApi.connectionInformationApi.getConnectionStateFlow(), - synchronizationApi.getSynchronizationState() - ) { versionInformation, connectionState, synchronizationState -> - return@combine if (connectionState is ConnectionState.Disconnected) { - InfraredEmulateState.NOT_CONNECTED - } else if (connectionState !is ConnectionState.Ready || - connectionState.supportedState != FlipperSupportedState.READY - ) { - InfraredEmulateState.CONNECTING - } else if (synchronizationState is SynchronizationState.InProgress) { - InfraredEmulateState.SYNCING - } else if (versionInformation == null || - versionInformation < API_SUPPORTED_INFRARED_EMULATE - ) { - InfraredEmulateState.UPDATE_FLIPPER - } else { - InfraredEmulateState.ALL_GOOD - } - }.onEach { - info { "#onServiceApiReady $it" } - emulateStateFlow.emit(it) - }.launchIn(viewModelScope) + infraredConnectionApi.getState(serviceApi) + .onEach { info { "#onServiceApiReady $it" } } + .onEach { emulateStateFlow.emit(it) } + .launchIn(viewModelScope) } @AssistedFactory @@ -79,7 +56,3 @@ class InfraredViewModel @AssistedInject constructor( ): InfraredViewModel } } - -enum class InfraredEmulateState { - NOT_CONNECTED, CONNECTING, SYNCING, UPDATE_FLIPPER, ALL_GOOD -} 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 f2435f5430..8556b84a2b 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 @@ -12,7 +12,7 @@ abstract class BrandsScreenDecomposeComponent( componentContext: ComponentContext, categoryId: Long, onBackClick: () -> Unit, - onBrandClick: (brandId: Long) -> Unit + onBrandClick: (brandId: Long, brandName: String) -> Unit ): BrandsScreenDecomposeComponent } } diff --git a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/BrandsDecomposeComponent.kt b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/BrandsDecomposeComponent.kt index 23d6ee3e0f..ea6162c862 100644 --- a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/BrandsDecomposeComponent.kt +++ b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/BrandsDecomposeComponent.kt @@ -51,7 +51,7 @@ interface BrandsDecomposeComponent { componentContext: ComponentContext, categoryId: Long, onBackClick: DecomposeOnBackParameter, - onBrandClick: (brandId: Long) -> Unit + onBrandClick: (brandId: Long, brandName: String) -> Unit ): BrandsDecomposeComponent } } diff --git a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/internal/BrandsDecomposeComponentImpl.kt b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/internal/BrandsDecomposeComponentImpl.kt index 8597168c97..b30d09fb45 100644 --- a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/internal/BrandsDecomposeComponentImpl.kt +++ b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/internal/BrandsDecomposeComponentImpl.kt @@ -25,7 +25,7 @@ import javax.inject.Provider class BrandsDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @Assisted private val onBackClick: DecomposeOnBackParameter, - @Assisted private val onBrandClick: (brandId: Long) -> Unit, + @Assisted private val onBrandClick: (brandId: Long, brandName: String) -> Unit, @Assisted categoryId: Long, createBrandsListViewModel: BrandsListViewModel.Factory, createQueryViewModel: Provider @@ -82,7 +82,7 @@ class BrandsDecomposeComponentImpl @AssistedInject constructor( } override fun onBrandClick(brandModel: BrandModel) { - onBrandClick.invoke(brandModel.id) + onBrandClick.invoke(brandModel.id, brandModel.name) } override fun tryLoad() { diff --git a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/internal/BrandsScreenDecomposeComponentImpl.kt b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/internal/BrandsScreenDecomposeComponentImpl.kt index 72338ba384..bd7b577880 100644 --- a/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/internal/BrandsScreenDecomposeComponentImpl.kt +++ b/components/remote-controls/brands/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/brands/presentation/decompose/internal/BrandsScreenDecomposeComponentImpl.kt @@ -16,7 +16,7 @@ class BrandsScreenDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @Assisted categoryId: Long, @Assisted onBackClick: () -> Unit, - @Assisted onBrandClick: (brandId: Long) -> Unit, + @Assisted onBrandClick: (brandId: Long, brandName: String) -> Unit, brandsDecomposeComponentFactory: BrandsDecomposeComponent.Factory, ) : BrandsScreenDecomposeComponent(componentContext) { private val brandsComponent = brandsDecomposeComponentFactory.createBrandsComponent( 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 5fec769390..ace5e676c1 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 @@ -11,7 +11,7 @@ abstract class CategoriesScreenDecomposeComponent( operator fun invoke( componentContext: ComponentContext, onBackClick: () -> Unit, - onCategoryClick: (categoryId: Long) -> Unit + onCategoryClick: (categoryId: Long, categoryName: String) -> Unit ): CategoriesScreenDecomposeComponent } } diff --git a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/DeviceCategoriesComponent.kt b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/DeviceCategoriesComponent.kt index f85a362bbd..7034526730 100644 --- a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/DeviceCategoriesComponent.kt +++ b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/DeviceCategoriesComponent.kt @@ -25,7 +25,7 @@ interface DeviceCategoriesComponent { fun invoke( componentContext: ComponentContext, onBackClick: DecomposeOnBackParameter, - onCategoryClick: (categoryId: Long) -> Unit + onCategoryClick: (categoryId: Long, categoryName: String) -> Unit ): DeviceCategoriesComponent } } diff --git a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/internal/CategoriesScreenDecomposeComponentImpl.kt b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/internal/CategoriesScreenDecomposeComponentImpl.kt index c3a06b4ff2..ceb8531fc5 100644 --- a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/internal/CategoriesScreenDecomposeComponentImpl.kt +++ b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/internal/CategoriesScreenDecomposeComponentImpl.kt @@ -15,7 +15,7 @@ import me.gulya.anvil.assisted.ContributesAssistedFactory class CategoriesScreenDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @Assisted onBackClick: () -> Unit, - @Assisted onCategoryClick: (categoryId: Long) -> Unit, + @Assisted onCategoryClick: (categoryId: Long, categoryName: String) -> Unit, deviceCategoriesComponentFactory: DeviceCategoriesComponent.Factory, ) : CategoriesScreenDecomposeComponent(componentContext) { private val deviceCategoriesComponent = deviceCategoriesComponentFactory.invoke( diff --git a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/internal/DeviceCategoriesComponentImpl.kt b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/internal/DeviceCategoriesComponentImpl.kt index a74f8b9a5b..57dc03e16b 100644 --- a/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/internal/DeviceCategoriesComponentImpl.kt +++ b/components/remote-controls/categories/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/categories/presentation/decompose/internal/DeviceCategoriesComponentImpl.kt @@ -16,7 +16,7 @@ import javax.inject.Provider class DeviceCategoriesComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @Assisted private val onBackClick: DecomposeOnBackParameter, - @Assisted private val onCategoryClick: (categoryId: Long) -> Unit, + @Assisted private val onCategoryClick: (categoryId: Long, categoryName: String) -> Unit, createDeviceCategoryListViewModel: Provider, ) : DeviceCategoriesComponent, ComponentContext by componentContext { @@ -27,7 +27,10 @@ class DeviceCategoriesComponentImpl @AssistedInject constructor( override val model = deviceCategoryListFeature.model override fun onCategoryClick(category: DeviceCategory) { - onCategoryClick(category.id) + onCategoryClick( + category.id, + category.meta.manifest.singularDisplayName + ) } override fun onBackClick() = onBackClick.invoke() 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 f8f02f69c3..40b442acd5 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 @@ -23,12 +23,14 @@ fun ButtonItemComposable( emulatedKeyIdentifier: IfrKeyIdentifier?, onKeyDataClick: (IfrKeyIdentifier) -> Unit, isSyncing: Boolean, + isConnected: Boolean, modifier: Modifier = Modifier ) { when (buttonData) { is IconButtonData -> { ButtonPlaceholderComposition( isSyncing = isSyncing, + isConnected = isConnected, isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, content = { SquareIconButton( @@ -43,6 +45,7 @@ fun ButtonItemComposable( is ChannelButtonData -> { ButtonPlaceholderComposition( isSyncing = isSyncing, + isConnected = isConnected, isEmulating = buttonData.reduceKeyIdentifier == emulatedKeyIdentifier || buttonData.addKeyIdentifier == emulatedKeyIdentifier, content = { @@ -58,6 +61,7 @@ fun ButtonItemComposable( is VolumeButtonData -> { ButtonPlaceholderComposition( isSyncing = isSyncing, + isConnected = isConnected, isEmulating = buttonData.reduceKeyIdentifier == emulatedKeyIdentifier || buttonData.addKeyIdentifier == emulatedKeyIdentifier, content = { @@ -73,6 +77,7 @@ fun ButtonItemComposable( is NavigationButtonData -> { ButtonPlaceholderComposition( isSyncing = isSyncing, + isConnected = isConnected, isEmulating = listOf( buttonData.okKeyIdentifier, buttonData.upKeyIdentifier, @@ -96,6 +101,7 @@ fun ButtonItemComposable( is TextButtonData -> { ButtonPlaceholderComposition( isSyncing = isSyncing, + isConnected = isConnected, isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, content = { TextButton( @@ -111,6 +117,7 @@ fun ButtonItemComposable( is Base64ImageButtonData -> { ButtonPlaceholderComposition( isSyncing = isSyncing, + isConnected = isConnected, isEmulating = emulatedKeyIdentifier == buttonData.keyIdentifier, content = { Base64ImageButton( @@ -125,6 +132,7 @@ fun ButtonItemComposable( UnknownButtonData -> { ButtonPlaceholderComposition( isSyncing = isSyncing, + isConnected = isConnected, isEmulating = false, content = { UnknownButton( 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 f8eb1f28c0..a7b964ba5a 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/DoubleButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/DoubleButton.kt @@ -9,7 +9,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.ifrmvp.core.ui.button.core.LocalButtonPlaceholder import com.flipperdevices.ifrmvp.core.ui.button.core.SyncingBox import com.flipperdevices.ifrmvp.core.ui.button.core.TextButton import com.flipperdevices.ifrmvp.core.ui.layout.core.sf @@ -47,7 +46,7 @@ fun DoubleButton( text = lastText, background = LocalPalletV2.current.surface.menu.body.dufault, ) - SyncingBox(LocalButtonPlaceholder.current.isSyncing) + SyncingBox() } } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/NavigationButton.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/NavigationButton.kt index 6cd9f8db23..9ddcad5bd8 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/NavigationButton.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/NavigationButton.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.ifrmvp.core.ui.button.core.LocalButtonPlaceholder +import com.flipperdevices.ifrmvp.core.ui.button.core.NoConnectionBox import com.flipperdevices.ifrmvp.core.ui.button.core.SyncingBox import com.flipperdevices.ifrmvp.core.ui.layout.core.sf import com.flipperdevices.ifrmvp.core.ui.layout.core.sfp @@ -113,6 +113,7 @@ fun NavigationButton( .clickable(onClick = onDownClick) .align(Alignment.BottomCenter) ) - SyncingBox(isSyncing = LocalButtonPlaceholder.current.isSyncing) + SyncingBox() + NoConnectionBox() } } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/EmulatingBox.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/EmulatingBox.kt new file mode 100644 index 0000000000..1be6b6bf03 --- /dev/null +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/EmulatingBox.kt @@ -0,0 +1,66 @@ +package com.flipperdevices.ifrmvp.core.ui.button.core + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.flipperdevices.core.ui.ktx.placeholderConnecting +import com.flipperdevices.core.ui.theme.LocalPalletV2 + +@Composable +internal fun EmulatingBox( + modifier: Modifier = Modifier, + isEmulating: Boolean = LocalButtonPlaceholder.current.isEmulating, +) { + Crossfade(isEmulating) { isEmulatingLocal -> + if (isEmulatingLocal) { + Box( + modifier + .fillMaxSize() + .placeholderConnecting() + ) + } + } +} + +@Composable +internal fun NoConnectionBox( + modifier: Modifier = Modifier, + isConnected: Boolean = LocalButtonPlaceholder.current.isConnected, +) { + Crossfade(isConnected) { isConnectedLocal -> + if (!isConnectedLocal) { + Box( + modifier + .fillMaxSize() + .background(LocalPalletV2.current.action.blackAndWhite.icon.disabled.copy(alpha = 0.5f)) + ) + } + } +} + +@Composable +internal fun SyncingBox( + modifier: Modifier = Modifier, + isSyncing: Boolean = LocalButtonPlaceholder.current.isSyncing, +) { + Crossfade( + targetState = isSyncing, + modifier = modifier + ) { isEmulating -> + if (isEmulating) { + Box( + Modifier + .fillMaxSize() + .background(LocalPalletV2.current.surface.menu.body.dufault) + ) + Box( + Modifier + .fillMaxSize() + .placeholderConnecting() + ) + } + } +} diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/LocalButtonPlaceholder.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/LocalButtonPlaceholder.kt index a37e961db6..9b09046c57 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/LocalButtonPlaceholder.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/button/core/LocalButtonPlaceholder.kt @@ -8,21 +8,25 @@ import androidx.compose.runtime.compositionLocalOf val LocalButtonPlaceholder = compositionLocalOf { ButtonPlaceholderState.NONE } enum class ButtonPlaceholderState { - NONE, SYNCING, EMULATING; + NONE, NOT_CONNECTED, SYNCING, EMULATING; val isEmulating: Boolean get() = this == EMULATING val isSyncing: Boolean get() = this == SYNCING + val isConnected: Boolean + get() = this != NOT_CONNECTED } @Composable internal fun ButtonPlaceholderComposition( isSyncing: Boolean, isEmulating: Boolean, + isConnected: Boolean, content: @Composable () -> Unit ) { val buttonPlaceholderState = when { + !isConnected -> ButtonPlaceholderState.NOT_CONNECTED isSyncing -> ButtonPlaceholderState.SYNCING isEmulating -> ButtonPlaceholderState.EMULATING else -> ButtonPlaceholderState.NONE 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 aa28eb9d8b..dc729733e2 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,11 +1,9 @@ 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 @@ -13,7 +11,6 @@ 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.core.ui.theme.LocalPalletV2 import com.flipperdevices.ifrmvp.core.ui.layout.core.sf import com.flipperdevices.ifrmvp.core.ui.util.GridConstants @@ -30,11 +27,19 @@ fun SquareButton( .size(GridConstants.DEFAULT_BUTTON_SIZE.sf) .clip(RoundedCornerShape(8.sf)) .background(background) + .then( + if (!LocalButtonPlaceholder.current.isConnected) { + Modifier.background(LocalPalletV2.current.action.blackAndWhite.icon.disabled) + } else { + Modifier + } + ) .then( if (onClick != null) { Modifier.clickable( onClick = onClick, - enabled = !LocalButtonPlaceholder.current.isEmulating + enabled = (!LocalButtonPlaceholder.current.isEmulating) + .and(LocalButtonPlaceholder.current.isConnected) ) } else { Modifier @@ -43,48 +48,9 @@ fun SquareButton( contentAlignment = Alignment.Center, content = { content.invoke(this) - EmulatingBox(isEmulating = LocalButtonPlaceholder.current.isEmulating) - SyncingBox(isSyncing = LocalButtonPlaceholder.current.isSyncing) + EmulatingBox() + SyncingBox() + NoConnectionBox() } ) } - -@Composable -internal fun EmulatingBox( - isEmulating: Boolean, - modifier: Modifier = Modifier -) { - Crossfade(isEmulating) { isEmulatingLocal -> - if (isEmulatingLocal) { - Box( - modifier - .fillMaxSize() - .placeholderConnecting() - ) - } - } -} - -@Composable -internal fun SyncingBox( - isSyncing: Boolean, - modifier: Modifier = Modifier -) { - Crossfade( - targetState = isSyncing, - modifier = modifier - ) { isEmulating -> - if (isEmulating) { - Box( - Modifier - .fillMaxSize() - .background(LocalPalletV2.current.surface.menu.body.dufault) - ) - Box( - Modifier - .fillMaxSize() - .placeholderConnecting() - ) - } - } -} diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/ButtonsComposable.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/ButtonsComposable.kt index bef3d6cb04..9d94a1b136 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/ButtonsComposable.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/ButtonsComposable.kt @@ -26,6 +26,7 @@ internal fun BoxWithConstraintsScope.ButtonsComposable( pageLayout: PageLayout?, emulatedKeyIdentifier: IfrKeyIdentifier?, isSyncing: Boolean, + isConnected: Boolean, onButtonClick: (IfrButton, IfrKeyIdentifier) -> Unit, modifier: Modifier = Modifier, onReload: (() -> Unit)? = null, @@ -60,6 +61,7 @@ internal fun BoxWithConstraintsScope.ButtonsComposable( buttonData = button.data, emulatedKeyIdentifier = emulatedKeyIdentifier, isSyncing = isSyncing, + isConnected = isConnected, onKeyDataClick = { keyIdentifier -> onButtonClick.invoke(button, keyIdentifier) } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/GridPagesContent.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/GridPagesContent.kt index 9fce83a6e3..c29295155c 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/GridPagesContent.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/GridPagesContent.kt @@ -17,6 +17,7 @@ fun GridPagesContent( onButtonClick: (IfrButton, IfrKeyIdentifier) -> Unit, emulatedKeyIdentifier: IfrKeyIdentifier?, isSyncing: Boolean, + isConnected: Boolean, modifier: Modifier = Modifier, onReload: (() -> Unit)? = null, ) { @@ -29,7 +30,8 @@ fun GridPagesContent( emulatedKeyIdentifier = emulatedKeyIdentifier, onButtonClick = onButtonClick, onReload = onReload, - isSyncing = isSyncing + isSyncing = isSyncing, + isConnected = isConnected ) } ) @@ -47,7 +49,8 @@ private fun LoadedContentEmptyPreview() { onButtonClick = { _, _ -> }, onReload = {}, emulatedKeyIdentifier = null, - isSyncing = false + isSyncing = false, + isConnected = true ) } } diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/LoadingComposable.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/LoadingComposable.kt deleted file mode 100644 index dba9381eaf..0000000000 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/layout/shared/LoadingComposable.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.flipperdevices.ifrmvp.core.ui.layout.shared - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.core.ui.theme.LocalTypography -import kotlin.math.roundToInt - -private const val MAX_PROGRESS = 100 - -@Composable -fun LoadingComposable( - modifier: Modifier = Modifier, - progress: Float = 0f -) { - val progressFormatter = remember(progress) { - "${(progress * MAX_PROGRESS).roundToInt()}%" - } - val isProgressShown = remember(progress) { - progress != 0f - } - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - content = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(14.dp, Alignment.CenterVertically), - modifier = Modifier.fillMaxSize() - ) { - CircularProgressIndicator( - color = LocalPalletV2.current.action.brand.background.primary.default, - modifier = Modifier.size(54.dp) - ) - Text( - text = progressFormatter.takeIf { isProgressShown }.orEmpty(), - style = LocalTypography.current.bodySB14, - color = MaterialTheme.colors.onPrimary, - textAlign = TextAlign.Center - ) - } - } - ) -} 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 6386cc6bef..c33c3f4347 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 @@ -60,7 +60,7 @@ fun SharedTopBar( Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .weight(weight = 3f, fill = false) + .weight(weight = 2f, fill = false) .padding(horizontal = 8.dp) ) { Text( @@ -122,7 +122,10 @@ private fun SharedTopBarPreview() { subtitle = "Subtitle", onBackClick = {}, actions = { - Row(modifier = Modifier) { + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { Text( text = "Action", color = LocalPalletV2.current.text.title.blackOnColor, diff --git a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/LoadedContentPreview.kt b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/LoadedContentPreview.kt index 867bf6f614..36c42f973a 100644 --- a/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/LoadedContentPreview.kt +++ b/components/remote-controls/core-ui/src/main/kotlin/com/flipperdevices/ifrmvp/core/ui/preview/LoadedContentPreview.kt @@ -17,7 +17,8 @@ private fun LoadedContentPreview() { onButtonClick = { _, _ -> }, onReload = {}, emulatedKeyIdentifier = null, - isSyncing = false + isSyncing = false, + isConnected = true ) } } diff --git a/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt index fe40cd0cd5..2650fa0370 100644 --- a/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt +++ b/components/remote-controls/grid/create-control/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/createcontrol/composable/CreateControlComposable.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.theme.FlipperThemeInternal @@ -41,7 +42,8 @@ internal fun CreateControlComposable() { Text( text = stringResource(R.string.configuring_desc), color = LocalPalletV2.current.text.body.secondary, - style = LocalTypography.current.subtitleM12 + style = LocalTypography.current.subtitleM12, + textAlign = TextAlign.Center ) } } diff --git a/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/model/ServerRemoteControlParam.kt b/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/model/ServerRemoteControlParam.kt index ca1b3c8804..4c0a5c6fdb 100644 --- a/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/model/ServerRemoteControlParam.kt +++ b/components/remote-controls/grid/main/api/src/main/kotlin/com/flipperdevices/remotecontrols/api/model/ServerRemoteControlParam.kt @@ -3,7 +3,10 @@ package com.flipperdevices.remotecontrols.api.model import com.flipperdevices.bridge.dao.api.model.FlipperKeyType import com.flipperdevices.bridge.dao.api.model.UI_INFRARED_EXTENSION -data class ServerRemoteControlParam(val infraredFileId: Long) { +data class ServerRemoteControlParam( + val infraredFileId: Long, + val remoteName: String +) { val key: String get() = this.toString() @@ -12,8 +15,8 @@ data class ServerRemoteControlParam(val infraredFileId: Long) { get() = "${FlipperKeyType.INFRARED.flipperDir}/temp/" val nameWithExtension: String - get() = "$infraredFileId.ir" + get() = "$remoteName.ir" val uiFileNameWithExtension: String - get() = "$infraredFileId.$UI_INFRARED_EXTENSION" + get() = "$remoteName.$UI_INFRARED_EXTENSION" } diff --git a/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/ConfigureGridDecomposeComponentImpl.kt b/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/ConfigureGridDecomposeComponentImpl.kt index ac9cff33ca..6373622fc2 100644 --- a/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/ConfigureGridDecomposeComponentImpl.kt +++ b/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/ConfigureGridDecomposeComponentImpl.kt @@ -34,7 +34,10 @@ class ConfigureGridDecomposeComponentImpl @AssistedInject constructor( override val stack: Value> = childStack( source = navigation, serializer = GridNavigationConfig.serializer(), - initialConfiguration = GridNavigationConfig.ServerControl(param.infraredFileId), + initialConfiguration = GridNavigationConfig.ServerControl( + id = param.infraredFileId, + remoteName = param.remoteName + ), handleBackButton = true, childFactory = ::child, ) @@ -65,7 +68,10 @@ class ConfigureGridDecomposeComponentImpl @AssistedInject constructor( is GridNavigationConfig.ServerControl -> remoteGridComponentFactory.invoke( componentContext = componentContext, - param = ServerRemoteControlParam(config.id), + param = ServerRemoteControlParam( + config.id, + config.remoteName + ), onBack = { navigation.popOr(onBack::invoke) }, onSaveKey = { navigation.pushNew(GridNavigationConfig.Rename(it)) diff --git a/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/model/GridNavigationConfig.kt b/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/model/GridNavigationConfig.kt index f1b8f12182..dbe00f32b6 100644 --- a/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/model/GridNavigationConfig.kt +++ b/components/remote-controls/grid/main/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/main/model/GridNavigationConfig.kt @@ -10,7 +10,7 @@ sealed class GridNavigationConfig { data class Rename(val notSavedFlipperKey: NotSavedFlipperKey) : GridNavigationConfig() @Serializable - data class ServerControl(val id: Long) : GridNavigationConfig() + data class ServerControl(val id: Long, val remoteName: String) : GridNavigationConfig() @Serializable data class Configuring( diff --git a/components/remote-controls/grid/remote/impl/build.gradle.kts b/components/remote-controls/grid/remote/impl/build.gradle.kts index 41968581be..95a891c25d 100644 --- a/components/remote-controls/grid/remote/impl/build.gradle.kts +++ b/components/remote-controls/grid/remote/impl/build.gradle.kts @@ -18,7 +18,9 @@ dependencies { implementation(projects.components.core.ui.dialog) implementation(projects.components.bridge.dao.api) + implementation(projects.components.bridge.service.api) implementation(projects.components.infrared.utils) + implementation(projects.components.infrared.api) implementation(projects.components.remoteControls.apiBackend) implementation(projects.components.remoteControls.coreModel) diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/RemoteGridComposable.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/RemoteGridComposable.kt index 362e13dbae..d2c1f272ba 100644 --- a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/RemoteGridComposable.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/RemoteGridComposable.kt @@ -1,10 +1,7 @@ package com.flipperdevices.remotecontrols.impl.grid.remote.composable -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold -import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -12,15 +9,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import com.flipperdevices.core.ui.ktx.clickableRipple import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.core.ui.theme.LocalTypography -import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar -import com.flipperdevices.remotecontrols.grid.remote.impl.R import com.flipperdevices.remotecontrols.impl.grid.remote.composable.components.RemoteGridComposableContent +import com.flipperdevices.remotecontrols.impl.grid.remote.composable.components.RemoteGridTopBar import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent @Composable @@ -36,23 +27,14 @@ fun RemoteGridComposable( Scaffold( modifier = modifier, topBar = { - SharedTopBar( - onBackClick = remoteGridComponent::pop, - actions = { - AnimatedVisibility(model.isFilesSaved) { - Row(modifier = Modifier) { - Text( - text = stringResource(R.string.save), - color = LocalPalletV2.current.text.title.blackOnColor, - style = LocalTypography.current.titleEB18, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.clickableRipple(onClick = remoteGridComponent::save) - ) - } - } - } + RemoteGridTopBar( + isFilesSaved = model.isFilesSaved, + onBack = remoteGridComponent::pop, + remoteName = (model as? RemoteGridComponent.Model.Loaded)?.let { + remoteGridComponent.param.remoteName + }, + onSave = remoteGridComponent::save, + saveProgress = (model as? RemoteGridComponent.Model.Loaded)?.saveProgressOrNull ) }, backgroundColor = LocalPalletV2.current.surface.backgroundMain.body, diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridComposableContent.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridComposableContent.kt index edb4c454ac..665a8f4592 100644 --- a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridComposableContent.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridComposableContent.kt @@ -4,13 +4,23 @@ 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.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.dialog.composable.busy.ComposableFlipperBusy +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable import com.flipperdevices.ifrmvp.core.ui.layout.shared.GridPagesContent -import com.flipperdevices.ifrmvp.core.ui.layout.shared.LoadingComposable import com.flipperdevices.remotecontrols.grid.remote.impl.R import com.flipperdevices.remotecontrols.impl.grid.remote.composable.util.contentKey import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent @@ -55,12 +65,28 @@ internal fun RemoteGridComposableContent( }, onReload = remoteGridComponent::tryLoad, emulatedKeyIdentifier = animatedModel.emulatedKey, - isSyncing = animatedModel.isSavingFiles + isSyncing = animatedModel.isSavingFiles, + isConnected = animatedModel.isConnected ) } is RemoteGridComponent.Model.Loading -> { - LoadingComposable(progress = animatedModel.progress) + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = LocalPallet.current.accentSecond, + strokeWidth = 2.dp + ) + Text( + text = stringResource(R.string.loading_remote), + style = LocalTypography.current.bodySB14, + color = LocalPalletV2.current.text.body.whiteOnColor + ) + } } } } diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridTopBar.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridTopBar.kt new file mode 100644 index 0000000000..065185fa05 --- /dev/null +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/composable/components/RemoteGridTopBar.kt @@ -0,0 +1,59 @@ +package com.flipperdevices.remotecontrols.impl.grid.remote.composable.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar +import com.flipperdevices.remotecontrols.grid.remote.impl.R + +@Composable +internal fun RemoteGridTopBar( + isFilesSaved: Boolean, + remoteName: String?, + saveProgress: Int?, + onBack: () -> Unit, + onSave: () -> Unit, +) { + SharedTopBar( + onBackClick = onBack, + title = remoteName.orEmpty(), + subtitle = when { + saveProgress == null -> { + stringResource(R.string.remote_subtitle) + } + + else -> { + stringResource(R.string.uploading_to_flipper).format("$saveProgress%") + } + }, + actions = { + AnimatedVisibility( + visible = isFilesSaved, + enter = fadeIn(), + exit = fadeOut() + ) { + Row(modifier = Modifier) { + Text( + text = stringResource(R.string.save), + color = LocalPalletV2.current.text.title.blackOnColor, + style = LocalTypography.current.titleEB18, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.clickableRipple(onClick = onSave) + ) + } + } + } + ) +} diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/RemoteGridComponent.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/RemoteGridComponent.kt index 33d6010aec..080ca003e5 100644 --- a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/RemoteGridComponent.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/RemoteGridComponent.kt @@ -3,8 +3,10 @@ package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompos import com.arkivanov.decompose.ComponentContext import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.ifrmvp.model.PagesLayout +import com.flipperdevices.infrared.api.InfraredConnectionApi.InfraredEmulateState import com.flipperdevices.infrared.editor.core.model.InfraredRemote import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import com.flipperdevices.remotecontrols.api.SaveTempSignalApi import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import kotlinx.collections.immutable.ImmutableList @@ -12,6 +14,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow interface RemoteGridComponent { + val param: ServerRemoteControlParam + fun model(coroutineScope: CoroutineScope): StateFlow fun onButtonClick(identifier: IfrKeyIdentifier) @@ -33,8 +37,17 @@ interface RemoteGridComponent { val remotes: ImmutableList, val isFlipperBusy: Boolean = false, val emulatedKey: IfrKeyIdentifier? = null, - val isSavingFiles: Boolean, - ) : Model + val saveState: SaveTempSignalApi.State, + val connectionState: InfraredEmulateState + ) : Model { + val isConnected = connectionState != InfraredEmulateState.NOT_CONNECTED + val isSavingFiles = saveState is SaveTempSignalApi.State.Uploading + val saveProgressOrNull = (saveState as? SaveTempSignalApi.State.Uploading) + ?.progressPercent + ?.times(other = 100) + ?.toInt() + ?.coerceIn(minimumValue = 0, maximumValue = 100) + } data object Error : Model diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridComponentImpl.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridComponentImpl.kt index 2f3edcbd34..0d256cfa85 100644 --- a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridComponentImpl.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/decompose/internal/RemoteGridComponentImpl.kt @@ -13,6 +13,7 @@ import com.flipperdevices.remotecontrols.api.SaveTempSignalApi import com.flipperdevices.remotecontrols.api.model.ServerRemoteControlParam import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.mapping.GridComponentStateMapper +import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.viewmodel.ConnectionViewModel import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.viewmodel.RemoteGridViewModel import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import dagger.assisted.Assisted @@ -28,12 +29,13 @@ import javax.inject.Provider @ContributesAssistedFactory(AppGraph::class, RemoteGridComponent.Factory::class) class RemoteGridComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, - @Assisted private val param: ServerRemoteControlParam, + @Assisted override val param: ServerRemoteControlParam, @Assisted private val onBack: DecomposeOnBackParameter, @Assisted private val onSaveKey: (NotSavedFlipperKey) -> Unit, createRemoteGridViewModel: RemoteGridViewModel.Factory, createSaveTempSignalApi: Provider, createDispatchSignalApi: Provider, + createConnectionViewModel: Provider ) : RemoteGridComponent, ComponentContext by componentContext { private val saveTempSignalApi = instanceKeeper.getOrCreate( key = "GridComponent_saveSignalViewModel_${param.key}", @@ -41,6 +43,12 @@ class RemoteGridComponentImpl @AssistedInject constructor( createSaveTempSignalApi.get() } ) + private val connectionViewModel = instanceKeeper.getOrCreate( + key = "GridComponent_connectionViewModel_${param.key}", + factory = { + createConnectionViewModel.get() + } + ) private val dispatchSignalApi = instanceKeeper.getOrCreate( key = "GridComponent_dispatchSignalViewModel_${param.key}", factory = { @@ -78,11 +86,13 @@ class RemoteGridComponentImpl @AssistedInject constructor( saveTempSignalApi.state, remoteGridViewModel.state, dispatchSignalApi.state, - transform = { saveState, gridState, dispatchState -> + connectionViewModel.state, + transform = { saveState, gridState, dispatchState, connectionState -> GridComponentStateMapper.map( saveState = saveState, gridState = gridState, dispatchState = dispatchState, + connectionState = connectionState ) } ).stateIn(coroutineScope, SharingStarted.Eagerly, RemoteGridComponent.Model.Loading()) diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/mapping/GridComponentStateMapper.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/mapping/GridComponentStateMapper.kt index 4dd3cea1da..53e6c2d5f2 100644 --- a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/mapping/GridComponentStateMapper.kt +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/mapping/GridComponentStateMapper.kt @@ -1,5 +1,6 @@ package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.mapping +import com.flipperdevices.infrared.api.InfraredConnectionApi import com.flipperdevices.remotecontrols.api.DispatchSignalApi import com.flipperdevices.remotecontrols.api.SaveTempSignalApi import com.flipperdevices.remotecontrols.impl.grid.remote.presentation.decompose.RemoteGridComponent @@ -10,6 +11,7 @@ internal object GridComponentStateMapper { saveState: SaveTempSignalApi.State, gridState: RemoteGridViewModel.State, dispatchState: DispatchSignalApi.State, + connectionState: InfraredConnectionApi.InfraredEmulateState ): RemoteGridComponent.Model = when (gridState) { RemoteGridViewModel.State.Error -> RemoteGridComponent.Model.Error is RemoteGridViewModel.State.Loaded -> { @@ -23,7 +25,8 @@ internal object GridComponentStateMapper { remotes = gridState.remotes, isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, emulatedKey = (dispatchState as? DispatchSignalApi.State.Emulating)?.ifrKeyIdentifier, - isSavingFiles = saveState is SaveTempSignalApi.State.Uploading, + saveState = saveState, + connectionState = connectionState ) } } diff --git a/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/viewmodel/ConnectionViewModel.kt b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/viewmodel/ConnectionViewModel.kt new file mode 100644 index 0000000000..55954bdaff --- /dev/null +++ b/components/remote-controls/grid/remote/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/remote/presentation/viewmodel/ConnectionViewModel.kt @@ -0,0 +1,23 @@ +package com.flipperdevices.remotecontrols.impl.grid.remote.presentation.viewmodel + +import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.infrared.api.InfraredConnectionApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +class ConnectionViewModel @Inject constructor( + private val infraredConnectionApi: InfraredConnectionApi, + serviceProvider: FlipperServiceProvider, +) : DecomposeViewModel() { + val state = flow { + val serviceApi = serviceProvider.getServiceApi() + infraredConnectionApi.getState(serviceApi).collect { emit(it) } + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, + InfraredConnectionApi.InfraredEmulateState.ALL_GOOD + ) +} diff --git a/components/remote-controls/grid/remote/impl/src/main/res/values/strings.xml b/components/remote-controls/grid/remote/impl/src/main/res/values/strings.xml index 2d1dc332e1..b5bc658c47 100644 --- a/components/remote-controls/grid/remote/impl/src/main/res/values/strings.xml +++ b/components/remote-controls/grid/remote/impl/src/main/res/values/strings.xml @@ -2,4 +2,7 @@ Page is empty! Save + Loading Remote… + Uploading to Flipper: %s + Remote \ No newline at end of file diff --git a/components/remote-controls/grid/saved/impl/build.gradle.kts b/components/remote-controls/grid/saved/impl/build.gradle.kts index 5f03351a0c..4c16559dd5 100644 --- a/components/remote-controls/grid/saved/impl/build.gradle.kts +++ b/components/remote-controls/grid/saved/impl/build.gradle.kts @@ -18,8 +18,10 @@ dependencies { implementation(projects.components.core.ui.dialog) implementation(projects.components.bridge.dao.api) + implementation(projects.components.bridge.service.api) implementation(projects.components.bridge.synchronization.api) implementation(projects.components.infrared.utils) + implementation(projects.components.infrared.api) implementation(projects.components.remoteControls.coreModel) implementation(projects.components.remoteControls.coreUi) diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/LocalGridComposable.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/LocalGridComposable.kt index 3e812ba98d..54230fc69c 100644 --- a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/LocalGridComposable.kt +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/LocalGridComposable.kt @@ -14,10 +14,12 @@ 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.unit.dp -import com.flipperdevices.bridge.synchronization.api.SynchronizationState import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar +import com.flipperdevices.infrared.api.InfraredConnectionApi.InfraredEmulateState +import com.flipperdevices.remotecontrols.grid.saved.impl.R import com.flipperdevices.remotecontrols.impl.grid.local.api.LocalGridScreenDecomposeComponent import com.flipperdevices.remotecontrols.impl.grid.local.composable.components.ComposableInfraredDropDown import com.flipperdevices.remotecontrols.impl.grid.local.composable.components.ComposableSynchronizationNotification @@ -46,7 +48,11 @@ fun LocalGridComposable( modifier = modifier, topBar = { (model as? LocalGridComponent.Model.Loaded)?.let { loadedModel -> - SharedTopBar(onBackClick = localGridComponent::pop) { + SharedTopBar( + onBackClick = localGridComponent::pop, + title = loadedModel.keyPath.path.nameWithoutExtension, + subtitle = stringResource(R.string.remote_subtitle) + ) { ComposableInfraredDropDown( onRename = { localGridComponent.onRename { @@ -62,11 +68,14 @@ fun LocalGridComposable( }, onRemoteInfo = { onCallback.invoke( - LocalGridScreenDecomposeComponent.Callback.ViewRemoteInfo(loadedModel.keyPath) + LocalGridScreenDecomposeComponent.Callback.ViewRemoteInfo( + loadedModel.keyPath + ) ) }, onShare = onShare, - emulatingInProgress = loadedModel.emulatedKey != null + isEmulating = loadedModel.emulatedKey != null, + isConnected = loadedModel.isConnected ) } } @@ -87,8 +96,8 @@ fun LocalGridComposable( contentAlignment = Alignment.BottomCenter ) { val state = (model as? LocalGridComponent.Model.Loaded) - ?.synchronizationState - ?: SynchronizationState.NotStarted + ?.connectionState + ?: InfraredEmulateState.ALL_GOOD ComposableSynchronizationNotification(state) } } diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/ComposableSyncNotification.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/ComposableSyncNotification.kt index 627be851e2..11aa3ac463 100644 --- a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/ComposableSyncNotification.kt +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/ComposableSyncNotification.kt @@ -1,6 +1,7 @@ package com.flipperdevices.remotecontrols.impl.grid.local.composable.components import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -12,7 +13,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -34,44 +34,40 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.flipperdevices.bridge.synchronization.api.SynchronizationState import com.flipperdevices.core.ui.theme.FlipperThemeInternal import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.infrared.api.InfraredConnectionApi.InfraredEmulateState import com.flipperdevices.remotecontrols.grid.saved.impl.R import kotlinx.coroutines.delay @Composable -internal fun ComposableNotification( - icon: @Composable RowScope.() -> Unit, - text: String, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(LocalPalletV2.current.surface.popUp.body.default) - .padding(14.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - icon.invoke(this) - Text( - modifier = Modifier - .padding(top = 9.dp, bottom = 9.dp, end = 12.dp) - .weight(1f), - text = text, - style = LocalTypography.current.subtitleB12 - ) +private fun notificationText(state: InfraredEmulateState): String { + return when (state) { + InfraredEmulateState.NOT_CONNECTED -> stringResource(R.string.sync_state_not_connected) + InfraredEmulateState.CONNECTING, + InfraredEmulateState.SYNCING -> stringResource(R.string.sync_state_syncing) + + InfraredEmulateState.UPDATE_FLIPPER, + InfraredEmulateState.ALL_GOOD -> stringResource(R.string.sync_state_synced) } } @Composable -fun ComposableSyncingNotification(modifier: Modifier = Modifier) { - ComposableNotification( - modifier = modifier, - icon = { +private fun NotificationIcon(state: InfraredEmulateState) { + when (state) { + InfraredEmulateState.NOT_CONNECTED -> { + Icon( + painter = painterResource(R.drawable.ic_not_connected), + contentDescription = null, + modifier = Modifier + .size(24.dp), + tint = LocalPalletV2.current.illustration.blackAndWhite.black + ) + } + + InfraredEmulateState.SYNCING, + InfraredEmulateState.CONNECTING -> { val transition = rememberInfiniteTransition() val angle by transition.animateFloat( label = "Icon rotation", @@ -92,16 +88,10 @@ fun ComposableSyncingNotification(modifier: Modifier = Modifier) { .rotate(angle), tint = LocalPalletV2.current.text.link.default ) - }, - text = stringResource(R.string.sync_state_syncing) - ) -} + } -@Composable -fun ComposableSyncedNotification(modifier: Modifier = Modifier) { - ComposableNotification( - modifier = modifier, - icon = { + InfraredEmulateState.UPDATE_FLIPPER, + InfraredEmulateState.ALL_GOOD -> { Icon( painter = painterResource(R.drawable.ic_synced), contentDescription = null, @@ -109,53 +99,62 @@ fun ComposableSyncedNotification(modifier: Modifier = Modifier) { .size(24.dp), tint = LocalPalletV2.current.text.link.default ) - }, - text = stringResource(R.string.sync_state_synced) - ) + } + } } @Composable -fun ComposableNotConnectedNotification(modifier: Modifier = Modifier) { - ComposableNotification( - modifier = modifier, - icon = { - Icon( - painter = painterResource(R.drawable.ic_not_connected), - contentDescription = null, +private fun ComposableNotification( + state: InfraredEmulateState, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(LocalPalletV2.current.surface.popUp.body.default) + .padding(14.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Crossfade(state) { state -> NotificationIcon(state) } + Crossfade(state) { state -> + Text( modifier = Modifier - .size(24.dp), - tint = LocalPalletV2.current.illustration.blackAndWhite.black + .padding(top = 9.dp, bottom = 9.dp, end = 12.dp) + .weight(1f), + text = notificationText(state), + style = LocalTypography.current.subtitleB12 ) - }, - text = stringResource(R.string.sync_state_not_connected) - ) + } + } } private const val VISIBILITY_DURATION = 2000L @Composable -fun ComposableSynchronizationNotification(state: SynchronizationState) { +fun ComposableSynchronizationNotification(state: InfraredEmulateState) { var isVisible by remember(Unit) { mutableStateOf(false) } LaunchedEffect(state, isVisible) { - if (state is SynchronizationState.NotStarted) isVisible = false - if (state is SynchronizationState.InProgress) isVisible = true - if (state is SynchronizationState.Finished) { - SnackbarDuration.Short - delay(VISIBILITY_DURATION) - isVisible = false + when (state) { + InfraredEmulateState.NOT_CONNECTED -> isVisible = true + InfraredEmulateState.CONNECTING -> isVisible = true + InfraredEmulateState.SYNCING -> isVisible = true + + InfraredEmulateState.UPDATE_FLIPPER, + InfraredEmulateState.ALL_GOOD -> { + if (!isVisible) return@LaunchedEffect + SnackbarDuration.Short + delay(VISIBILITY_DURATION) + isVisible = false + } } } AnimatedVisibility( visible = isVisible, enter = fadeIn(), exit = fadeOut(), - content = { - when (state) { - SynchronizationState.Finished -> ComposableSyncedNotification() - is SynchronizationState.InProgress -> ComposableSyncingNotification() - SynchronizationState.NotStarted -> Unit - } - } + content = { ComposableNotification(state) } ) } @@ -164,9 +163,9 @@ fun ComposableSynchronizationNotification(state: SynchronizationState) { private fun ComposableNotificationPreview() { FlipperThemeInternal { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - ComposableSyncingNotification() - ComposableSyncedNotification() - ComposableNotConnectedNotification() + ComposableNotification(InfraredEmulateState.CONNECTING) + ComposableNotification(InfraredEmulateState.NOT_CONNECTED) + ComposableNotification(InfraredEmulateState.ALL_GOOD) } } } diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/GridOptions.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/GridOptions.kt index fd1ba2551b..8d073a8ff4 100644 --- a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/GridOptions.kt +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/GridOptions.kt @@ -2,6 +2,7 @@ package com.flipperdevices.remotecontrols.impl.grid.local.composable.components +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -39,21 +40,33 @@ internal fun ComposableInfraredDropDown( onRename: () -> Unit, onShare: () -> Unit, onDelete: () -> Unit, - emulatingInProgress: Boolean, + isEmulating: Boolean, + isConnected: Boolean, modifier: Modifier = Modifier, ) { var isShowMoreOptions by remember { mutableStateOf(false) } val onChangeState = { isShowMoreOptions = !isShowMoreOptions } - + val isDropDownEnabled = isConnected && !isEmulating + val moreIconTint by animateColorAsState( + if (isDropDownEnabled) { + LocalPalletV2.current.icon.blackAndWhite.blackOnColor + } else { + LocalPalletV2.current.action.neutral.icon.primary.disabled + } + ) Row( modifier = modifier, horizontalArrangement = Arrangement.End ) { Icon( modifier = Modifier - .clickableRipple(bounded = false, onClick = onChangeState) + .clickableRipple( + bounded = false, + onClick = onChangeState, + enabled = isDropDownEnabled + ) .size(24.dp), - tint = LocalPalletV2.current.icon.blackAndWhite.blackOnColor, + tint = moreIconTint, painter = painterResource(SharedRes.drawable.ic_more_points), contentDescription = null ) @@ -69,6 +82,7 @@ internal fun ComposableInfraredDropDown( onChangeState.invoke() } ) + Divider(modifier = Modifier.padding(horizontal = 8.dp)) ComposableInfraredDropDownItem( text = stringResource(R.string.option_rename), painter = painterResource(R.drawable.ic_edit), @@ -76,10 +90,9 @@ internal fun ComposableInfraredDropDown( onChangeState.invoke() onRename.invoke() }, - isActive = !emulatingInProgress + isActive = !isEmulating ) Divider(modifier = Modifier.padding(horizontal = 8.dp)) - Divider(modifier = Modifier.padding(horizontal = 8.dp)) ComposableInfraredDropDownItem( text = stringResource(R.string.option_share), painter = painterResource(SharedRes.drawable.ic_upload), diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/LocalGridComposableContent.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/LocalGridComposableContent.kt index 21821504f0..1fa2b152e4 100644 --- a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/LocalGridComposableContent.kt +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/composable/components/LocalGridComposableContent.kt @@ -4,11 +4,23 @@ 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.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.dialog.composable.busy.ComposableFlipperBusy +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography import com.flipperdevices.ifrmvp.core.ui.layout.shared.GridPagesContent -import com.flipperdevices.ifrmvp.core.ui.layout.shared.LoadingComposable +import com.flipperdevices.remotecontrols.grid.saved.impl.R import com.flipperdevices.remotecontrols.impl.grid.local.composable.util.contentKey import com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.LocalGridComponent import com.flipperdevices.rootscreen.api.LocalRootNavigation @@ -47,12 +59,28 @@ internal fun LocalGridComposableContent( localGridComponent.onButtonClick(keyIdentifier) }, emulatedKeyIdentifier = animatedModel.emulatedKey, - isSyncing = animatedModel.isSynchronizing + isSyncing = animatedModel.isSynchronizing, + isConnected = animatedModel.isConnected ) } is LocalGridComponent.Model.Loading -> { - LoadingComposable() + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = LocalPallet.current.accentSecond, + strokeWidth = 2.dp + ) + Text( + text = stringResource(R.string.loading_remote), + style = LocalTypography.current.bodySB14, + color = LocalPalletV2.current.text.body.whiteOnColor + ) + } } } } diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/LocalGridComponent.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/LocalGridComponent.kt index 2f2ae2fdd5..21baa506d5 100644 --- a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/LocalGridComponent.kt +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/LocalGridComponent.kt @@ -2,9 +2,9 @@ package com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose import com.arkivanov.decompose.ComponentContext import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath -import com.flipperdevices.bridge.synchronization.api.SynchronizationState import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.ifrmvp.model.PagesLayout +import com.flipperdevices.infrared.api.InfraredConnectionApi.InfraredEmulateState import com.flipperdevices.infrared.editor.core.model.InfraredRemote import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import kotlinx.collections.immutable.ImmutableList @@ -26,10 +26,14 @@ interface LocalGridComponent { val remotes: ImmutableList, val isFlipperBusy: Boolean, val emulatedKey: IfrKeyIdentifier?, - val synchronizationState: SynchronizationState, + val connectionState: InfraredEmulateState, val keyPath: FlipperKeyPath ) : Model { - val isSynchronizing = synchronizationState is SynchronizationState.InProgress + val isSynchronizing = listOf( + InfraredEmulateState.SYNCING, + InfraredEmulateState.CONNECTING + ).contains(connectionState) + val isConnected = connectionState != InfraredEmulateState.NOT_CONNECTED } data object Error : Model diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridComponentImpl.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridComponentImpl.kt index 93de10d613..69f6d965ec 100644 --- a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridComponentImpl.kt +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/decompose/internal/LocalGridComponentImpl.kt @@ -3,11 +3,11 @@ package com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose import com.arkivanov.decompose.ComponentContext import com.arkivanov.essenty.instancekeeper.getOrCreate import com.flipperdevices.bridge.dao.api.model.FlipperKeyPath -import com.flipperdevices.bridge.synchronization.api.SynchronizationApi import com.flipperdevices.core.di.AppGraph import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.remotecontrols.api.DispatchSignalApi import com.flipperdevices.remotecontrols.impl.grid.local.presentation.decompose.LocalGridComponent +import com.flipperdevices.remotecontrols.impl.grid.local.presentation.viewmodel.ConnectionViewModel import com.flipperdevices.remotecontrols.impl.grid.local.presentation.viewmodel.LocalGridViewModel import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import dagger.assisted.Assisted @@ -26,7 +26,7 @@ class LocalGridComponentImpl @AssistedInject constructor( @Assisted private val onBack: DecomposeOnBackParameter, createLocalGridViewModel: LocalGridViewModel.Factory, createDispatchSignalApi: Provider, - private val synchronizationApi: SynchronizationApi + createConnectionViewModel: Provider, ) : LocalGridComponent, ComponentContext by componentContext { private val localGridViewModel = instanceKeeper.getOrCreate( key = "LocalGridComponent_localGridViewModel_$keyPath", @@ -36,12 +36,16 @@ class LocalGridComponentImpl @AssistedInject constructor( key = "LocalGridComponent_dispatchSignalApi_$keyPath", factory = { createDispatchSignalApi.get() } ) + private val connectionViewModel = instanceKeeper.getOrCreate( + key = "LocalGridComponent_connectionViewModel_$keyPath", + factory = { createConnectionViewModel.get() } + ) override fun model(coroutineScope: CoroutineScope) = combine( flow = localGridViewModel.state, flow2 = dispatchSignalApi.state, - flow3 = synchronizationApi.getSynchronizationState(), - transform = { gridState, dispatchState, syncState -> + flow3 = connectionViewModel.state, + transform = { gridState, dispatchState, connectionState -> when (gridState) { LocalGridViewModel.State.Error -> LocalGridComponent.Model.Error is LocalGridViewModel.State.Loaded -> LocalGridComponent.Model.Loaded( @@ -49,7 +53,7 @@ class LocalGridComponentImpl @AssistedInject constructor( remotes = gridState.remotes, isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, emulatedKey = (dispatchState as? DispatchSignalApi.State.Emulating)?.ifrKeyIdentifier, - synchronizationState = syncState, + connectionState = connectionState, keyPath = gridState.keyPath ) diff --git a/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/viewmodel/ConnectionViewModel.kt b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/viewmodel/ConnectionViewModel.kt new file mode 100644 index 0000000000..d27b62c296 --- /dev/null +++ b/components/remote-controls/grid/saved/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/grid/local/presentation/viewmodel/ConnectionViewModel.kt @@ -0,0 +1,23 @@ +package com.flipperdevices.remotecontrols.impl.grid.local.presentation.viewmodel + +import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.infrared.api.InfraredConnectionApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +class ConnectionViewModel @Inject constructor( + private val infraredConnectionApi: InfraredConnectionApi, + serviceProvider: FlipperServiceProvider, +) : DecomposeViewModel() { + val state = flow { + val serviceApi = serviceProvider.getServiceApi() + infraredConnectionApi.getState(serviceApi).collect { emit(it) } + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, + InfraredConnectionApi.InfraredEmulateState.ALL_GOOD + ) +} diff --git a/components/remote-controls/grid/saved/impl/src/main/res/values/strings.xml b/components/remote-controls/grid/saved/impl/src/main/res/values/strings.xml index c409e90a45..5d1dc21cae 100644 --- a/components/remote-controls/grid/saved/impl/src/main/res/values/strings.xml +++ b/components/remote-controls/grid/saved/impl/src/main/res/values/strings.xml @@ -8,4 +8,6 @@ Rename Share Delete + Loading Remote… + Remote \ No newline at end of file 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 0c86f26799..8b9cccd970 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 @@ -43,8 +43,11 @@ class RemoteControlsScreenDecomposeComponentImpl @AssistedInject constructor( categoriesScreenDecomposeComponentFactory( componentContext = componentContext, onBackClick = onBack::invoke, - onCategoryClick = { deviceCategoryId -> - val configuration = RemoteControlsNavigationConfig.Brands(deviceCategoryId) + onCategoryClick = { deviceCategoryId, categoryName -> + val configuration = RemoteControlsNavigationConfig.Brands( + deviceCategoryId, + categoryName + ) navigation.pushToFront(configuration) } ) @@ -55,10 +58,12 @@ class RemoteControlsScreenDecomposeComponentImpl @AssistedInject constructor( componentContext = componentContext, onBackClick = navigation::pop, categoryId = config.categoryId, - onBrandClick = { brandId -> + onBrandClick = { brandId, brandName -> val configuration = RemoteControlsNavigationConfig.Setup( categoryId = config.categoryId, - brandId = brandId + brandId = brandId, + categoryName = config.categoryName, + brandName = brandName ) navigation.pushToFront(configuration) } @@ -70,7 +75,9 @@ class RemoteControlsScreenDecomposeComponentImpl @AssistedInject constructor( componentContext = componentContext, param = SetupScreenDecomposeComponent.Param( brandId = config.brandId, - categoryId = config.categoryId + categoryId = config.categoryId, + brandName = config.brandName, + categoryName = config.categoryName ), 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 1d18b68f8b..97e8fad15c 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 @@ -10,8 +10,16 @@ sealed interface RemoteControlsNavigationConfig { data object SelectCategory : RemoteControlsNavigationConfig @Serializable - data class Brands(val categoryId: Long) : RemoteControlsNavigationConfig + data class Brands( + val categoryId: Long, + val categoryName: String + ) : RemoteControlsNavigationConfig @Serializable - class Setup(val categoryId: Long, val brandId: Long) : RemoteControlsNavigationConfig + class Setup( + val categoryId: Long, + val brandId: Long, + val categoryName: String, + val brandName: String + ) : RemoteControlsNavigationConfig } 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 d3b0a9dffa..47082c43d0 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 @@ -19,7 +19,7 @@ interface SaveTempSignalApi : InstanceKeeper.Instance { data object Pending : State data object Error : State data class Uploading(val progressInternal: Long, val total: Long) : State { - val progress: Float = if (total == 0L) 0f else progressInternal / total.toFloat() + val progressPercent: Float = if (total == 0L) 0f else progressInternal / total.toFloat() } data object Uploaded : 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 f6eb6c6730..029f84775e 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 @@ -19,5 +19,19 @@ abstract class SetupScreenDecomposeComponent( class Param( val brandId: Long, val categoryId: Long, - ) + val brandName: String, + val categoryName: String + ) { + val remoteName: String + get() = "${brandName}_$categoryName" + .replace(" ", "_") + .replace("\\", "_") + .replace("/", "_") + .replace(".", "_") + .take(MAX_SIZE_REMOTE_LENGTH) + } + + companion object { + private const val MAX_SIZE_REMOTE_LENGTH = 21 + } } diff --git a/components/remote-controls/setup/impl/build.gradle.kts b/components/remote-controls/setup/impl/build.gradle.kts index 7ea361ce66..4541b056fa 100644 --- a/components/remote-controls/setup/impl/build.gradle.kts +++ b/components/remote-controls/setup/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.components.bridge.api) implementation(projects.components.keyemulate.api) implementation(projects.components.infrared.utils) + implementation(projects.components.infrared.api) implementation(projects.components.remoteControls.apiBackend) implementation(projects.components.remoteControls.coreModel) 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 41d17033a1..787fe55403 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 @@ -11,7 +11,12 @@ interface SaveFileApi { ): Flow sealed interface Status { - data class Saving(val uploaded: Long, val size: Long) : Status + data class Saving( + val uploaded: Long, + val size: Long, + val lastWriteSize: Long + ) : Status + data object Finished : Status } } 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 f3db912f35..d7d266bcf5 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 @@ -50,8 +50,9 @@ class SaveFileApiImpl @Inject constructor() : LogTagProvider, SaveFileApi { val size = message.storageWriteRequest.file.data.size().toLong() progressInternal += size val status = SaveFileApi.Status.Saving( - progressInternal, - totalSize + uploaded = progressInternal, + size = totalSize, + lastWriteSize = size ) info { "#onSendCallback $status" } send(status) 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 6865e82b0b..04a4ddf3a5 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 @@ -34,6 +34,7 @@ private val SetupComponent.Model.key: Any is SetupComponent.Model.Loading -> "loading" } +@Suppress("LongMethod") @Composable fun SetupScreen( setupComponent: SetupComponent, @@ -47,7 +48,10 @@ fun SetupScreen( LaunchedEffect(setupComponent.remoteFoundFlow) { setupComponent.remoteFoundFlow.onEach { setupComponent.onFileFound(it) - val configuration = RootScreenConfig.ServerRemoteControl(it.id) + val configuration = RootScreenConfig.ServerRemoteControl( + it.id, + setupComponent.param.remoteName + ) rootNavigation.push(configuration) }.launchIn(this) } 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 5318b00b0c..bc0ceda09b 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 @@ -29,14 +29,16 @@ private fun SignalResponseButton( data: ButtonData, onClick: () -> Unit, emulatedKeyIdentifier: IfrKeyIdentifier?, - isSyncing: Boolean + isSyncing: Boolean, + isConnected: Boolean ) { ButtonItemComposable( buttonData = data, onKeyDataClick = { onClick.invoke() }, modifier = Modifier.size(64.dp), emulatedKeyIdentifier = emulatedKeyIdentifier, - isSyncing = isSyncing + isSyncing = isSyncing, + isConnected = isConnected, ) } @@ -45,6 +47,7 @@ fun ButtonContent( onClick: () -> Unit, data: ButtonData, isSyncing: Boolean, + isConnected: Boolean, emulatedKeyIdentifier: IfrKeyIdentifier?, categoryName: String, modifier: Modifier = Modifier, @@ -58,7 +61,8 @@ fun ButtonContent( data = data, onClick = onClick, emulatedKeyIdentifier = emulatedKeyIdentifier, - isSyncing = isSyncing + isSyncing = isSyncing, + isConnected = isConnected, ) Spacer(modifier = Modifier.height(14.dp)) Text( @@ -85,21 +89,24 @@ private fun ComposableConfirmContentDarkPreview() { categoryName = "CATEGORY", data = TextButtonData(text = "Hello"), emulatedKeyIdentifier = null, - isSyncing = false + isSyncing = false, + isConnected = true ) ButtonContent( onClick = {}, categoryName = "CATEGORY 2", data = TextButtonData(text = "TV/AV"), emulatedKeyIdentifier = null, - isSyncing = false + isSyncing = false, + isConnected = true ) ButtonContent( onClick = {}, categoryName = "CATEGORY 2", data = TextButtonData(text = "Hello world"), emulatedKeyIdentifier = null, - isSyncing = false + isSyncing = false, + isConnected = true ) } } 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 26370a390d..e6e3835668 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 @@ -16,6 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.flipperdevices.core.ui.theme.FlipperThemeInternal import com.flipperdevices.ifrmvp.backend.model.SignalResponseModel import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable +import com.flipperdevices.infrared.api.InfraredConnectionApi import com.flipperdevices.remotecontrols.impl.setup.presentation.decompose.SetupComponent import com.flipperdevices.remotecontrols.setup.impl.R as SetupR @@ -40,12 +41,13 @@ fun LoadedContent( data = signalResponse.data, categoryName = signalResponse.categoryName, emulatedKeyIdentifier = model.emulatedKeyIdentifier, - isSyncing = model.isSyncing + isSyncing = model.isSyncing, + isConnected = model.isConnected ) AnimatedVisibility( visible = model.isEmulated, enter = slideInVertically(initialOffsetY = { it / 2 }), - exit = slideOutVertically(), + exit = slideOutVertically(targetOffsetY = { it / 2 }), modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) @@ -82,7 +84,7 @@ private fun LoadedContentPreview() { response = SignalResponseModel(), isEmulated = true, emulatedKeyIdentifier = null, - isSyncing = false + connectionState = InfraredConnectionApi.InfraredEmulateState.ALL_GOOD ), 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 af6b9ac929..05f8aff364 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 @@ -4,6 +4,7 @@ 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.infrared.api.InfraredConnectionApi.InfraredEmulateState import com.flipperdevices.remotecontrols.api.SetupScreenDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import kotlinx.coroutines.CoroutineScope @@ -35,8 +36,14 @@ interface SetupComponent { val isFlipperBusy: Boolean = false, val emulatedKeyIdentifier: IfrKeyIdentifier?, val isEmulated: Boolean, - val isSyncing: Boolean - ) : Model + val connectionState: InfraredEmulateState + ) : Model { + val isSyncing: Boolean = listOf( + InfraredEmulateState.CONNECTING, + InfraredEmulateState.SYNCING + ).contains(connectionState) + val isConnected = connectionState != InfraredEmulateState.NOT_CONNECTED + } data object Error : Model } diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupComponentImpl.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/decompose/internal/SetupComponentImpl.kt index ccc484fbfd..b63b49371a 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 @@ -15,6 +15,7 @@ import com.flipperdevices.remotecontrols.api.SaveTempSignalApi import com.flipperdevices.remotecontrols.api.SetupScreenDecomposeComponent import com.flipperdevices.remotecontrols.impl.setup.presentation.decompose.SetupComponent import com.flipperdevices.remotecontrols.impl.setup.presentation.decompose.internal.mapping.toFFFormat +import com.flipperdevices.remotecontrols.impl.setup.presentation.viewmodel.ConnectionViewModel import com.flipperdevices.remotecontrols.impl.setup.presentation.viewmodel.CurrentSignalViewModel import com.flipperdevices.remotecontrols.impl.setup.presentation.viewmodel.HistoryViewModel import com.flipperdevices.ui.decompose.DecomposeOnBackParameter @@ -41,7 +42,8 @@ class SetupComponentImpl @AssistedInject constructor( currentSignalViewModelFactory: CurrentSignalViewModel.Factory, createHistoryViewModel: Provider, createSaveTempSignalApi: Provider, - createDispatchSignalApi: Provider + createDispatchSignalApi: Provider, + createConnectionViewModel: Provider ) : SetupComponent, ComponentContext by componentContext { private val saveSignalApi = instanceKeeper.getOrCreate( key = "SetupComponent_saveSignalApi_${param.brandId}_${param.categoryId}", @@ -49,6 +51,12 @@ class SetupComponentImpl @AssistedInject constructor( createSaveTempSignalApi.get() } ) + private val connectionViewModel = instanceKeeper.getOrCreate( + key = "SetupComponent_connectionViewModel_${param.brandId}_${param.categoryId}", + factory = { + createConnectionViewModel.get() + } + ) private val historyViewModel = instanceKeeper.getOrCreate( key = "SetupComponent_historyViewModel_${param.brandId}_${param.categoryId}", factory = { @@ -81,7 +89,8 @@ class SetupComponentImpl @AssistedInject constructor( saveSignalApi.state, dispatchSignalApi.state, dispatchSignalApi.isEmulated, - transform = { signalState, saveState, dispatchState, isEmulated -> + connectionViewModel.state, + transform = { signalState, saveState, dispatchState, isEmulated, connectionState -> val emulatingState = (dispatchState as? DispatchSignalApi.State.Emulating) when (signalState) { CurrentSignalViewModel.State.Error -> SetupComponent.Model.Error @@ -95,7 +104,7 @@ class SetupComponentImpl @AssistedInject constructor( isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy, emulatedKeyIdentifier = emulatingState?.ifrKeyIdentifier, isEmulated = isEmulated, - isSyncing = saveState is SaveTempSignalApi.State.Uploading + connectionState = connectionState ) } } diff --git a/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/ConnectionViewModel.kt b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/ConnectionViewModel.kt new file mode 100644 index 0000000000..6ccc42b1e6 --- /dev/null +++ b/components/remote-controls/setup/impl/src/main/kotlin/com/flipperdevices/remotecontrols/impl/setup/presentation/viewmodel/ConnectionViewModel.kt @@ -0,0 +1,23 @@ +package com.flipperdevices.remotecontrols.impl.setup.presentation.viewmodel + +import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.infrared.api.InfraredConnectionApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +class ConnectionViewModel @Inject constructor( + private val infraredConnectionApi: InfraredConnectionApi, + serviceProvider: FlipperServiceProvider, +) : DecomposeViewModel() { + val state = flow { + val serviceApi = serviceProvider.getServiceApi() + infraredConnectionApi.getState(serviceApi).collect { emit(it) } + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, + InfraredConnectionApi.InfraredEmulateState.ALL_GOOD + ) +} 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 0bf8013520..cfd88da999 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 @@ -40,7 +40,9 @@ class SaveTempSignalViewModel @Inject constructor( onFinished: () -> Unit, ) { viewModelScope.launch { - _state.emit(SaveTempSignalApi.State.Uploading(0, 0)) + var progressInternal = 0L + val totalSize = filesDesc.sumOf { it.textContent.toByteArray().size.toLong() } + _state.emit(SaveTempSignalApi.State.Uploading(0, totalSize)) val serviceApi = serviceProvider.getServiceApi() launchWithLock(mutex, viewModelScope, "load") { filesDesc.forEach { fileDesc -> @@ -60,10 +62,13 @@ class SaveTempSignalViewModel @Inject constructor( .onEach { _state.value = when (it) { SaveFileApi.Status.Finished -> SaveTempSignalApi.State.Uploaded - is SaveFileApi.Status.Saving -> SaveTempSignalApi.State.Uploading( - it.uploaded, - it.size - ) + is SaveFileApi.Status.Saving -> { + progressInternal += it.lastWriteSize + SaveTempSignalApi.State.Uploading( + progressInternal, + totalSize + ) + } } } .collect() 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 5a349f56fc..5e37d4c005 100644 --- a/components/rootscreen/api/src/main/kotlin/com/flipperdevices/rootscreen/model/RootScreenConfig.kt +++ b/components/rootscreen/api/src/main/kotlin/com/flipperdevices/rootscreen/model/RootScreenConfig.kt @@ -39,5 +39,8 @@ sealed class RootScreenConfig { data object RemoteControls : RootScreenConfig() @Serializable - data class ServerRemoteControl(val infraredFileId: Long) : RootScreenConfig() + data class ServerRemoteControl( + val infraredFileId: Long, + val remoteName: String + ) : RootScreenConfig() } 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 64c7b2a16c..8282859f28 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 @@ -141,7 +141,10 @@ class RootDecomposeComponentImpl @AssistedInject constructor( is RootScreenConfig.ServerRemoteControl -> serverRemoteControlFactory( componentContext = componentContext, - param = ServerRemoteControlParam(config.infraredFileId), + param = ServerRemoteControlParam( + infraredFileId = config.infraredFileId, + remoteName = config.remoteName + ), onBack = this::internalOnBack, ) }