Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Attention: don't forget to add the flag for F-Droid before release
- [Feature] Skip infrared signals on setup screen
- [Feature] Better user-ux when configuring remote control
- [Feature] Add flipper action dialogs into remote control and move it into bottombar
- [Feature] Add error display into remote controls screens
- [Refactor] Load RemoteControls from flipper, emulating animation
- [Refactor] Update to Kotlin 2.0
- [Refactor] Replace Ktorfit with Ktor requests in remote-controls
Expand Down
29 changes: 29 additions & 0 deletions components/remote-controls/api-backend-flipper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
plugins {
id("flipper.multiplatform")
id("flipper.multiplatform-dependencies")
id("kotlinx-serialization")
id("flipper.anvil")
}

android.namespace = "com.flipperdevices.remotecontrols.api.backend.flipper"

androidDependencies {
implementation(libs.kotlin.coroutines)
implementation(libs.kotlin.serialization.json)
implementation(projects.components.core.di)
implementation(projects.components.remoteControls.coreModel)
implementation(projects.components.remoteControls.apiBackend)
implementation(projects.components.faphub.target.api)
implementation(projects.components.faphub.errors.api)
implementation(projects.components.bridge.service.api)
implementation(projects.components.bridge.api)
implementation(projects.components.bridge.rpcinfo.api)
implementation(projects.components.bridge.rpc.api)

implementation(libs.dagger)
implementation(libs.square.anvil.annotations)
implementation(libs.ktor.client)
implementation(libs.ktor.serialization)
implementation(libs.ktor.logging)
implementation(libs.ktor.negotiation)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.flipperdevices.ifrmvp.api.infrared

/**
* This api will also check for [FapHubError]
*/
interface FlipperInfraredBackendApi : InfraredBackendApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.flipperdevices.ifrmvp.api.infrared.internal

import com.flipperdevices.bridge.api.manager.ktx.state.ConnectionState
import com.flipperdevices.bridge.rpc.api.model.exceptions.NoSdCardException
import com.flipperdevices.bridge.rpcinfo.api.FlipperStorageInformationApi
import com.flipperdevices.bridge.rpcinfo.model.FlipperInformationStatus
import com.flipperdevices.bridge.rpcinfo.model.StorageStats
import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.faphub.errors.api.throwable.FirmwareNotSupported
import com.flipperdevices.faphub.errors.api.throwable.FlipperNotConnected
import com.flipperdevices.faphub.target.api.FlipperTargetProviderApi
import com.flipperdevices.faphub.target.model.FlipperTarget
import com.flipperdevices.ifrmvp.api.infrared.FlipperInfraredBackendApi
import com.flipperdevices.ifrmvp.api.infrared.InfraredBackendApi
import com.flipperdevices.ifrmvp.backend.model.BrandsResponse
import com.flipperdevices.ifrmvp.backend.model.CategoriesResponse
import com.flipperdevices.ifrmvp.backend.model.IfrFileContentResponse
import com.flipperdevices.ifrmvp.backend.model.InfraredsResponse
import com.flipperdevices.ifrmvp.backend.model.PagesLayoutBackendModel
import com.flipperdevices.ifrmvp.backend.model.SignalRequestModel
import com.flipperdevices.ifrmvp.backend.model.SignalResponseModel
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import javax.inject.Inject

@ContributesBinding(AppGraph::class, FlipperInfraredBackendApi::class)
class FlipperInfraredBackendApiImpl @Inject constructor(
private val api: InfraredBackendApi,
private val flipperTargetProviderApi: FlipperTargetProviderApi,
private val flipperServiceProvider: FlipperServiceProvider,
private val flipperStorageInformationApi: FlipperStorageInformationApi,
) : FlipperInfraredBackendApi {
private suspend fun isSdCardPresent(): Boolean {
val stats = flipperStorageInformationApi.getStorageInformationFlow()
.map { fStorageInformation -> fStorageInformation.externalStorageStatus }
.filterIsInstance<FlipperInformationStatus.Ready<StorageStats?>>()
.map { fStatusInformation -> fStatusInformation.data }
.filterNotNull()
.first()
return stats is StorageStats.Loaded
}

private suspend fun isDeviceConnected(): Boolean {
return flipperServiceProvider.getServiceApi()
.connectionInformationApi
.getConnectionStateFlow()
.first() is ConnectionState.Ready
}

@Suppress("ThrowsCount", "RethrowCaughtException")
private suspend fun <T> wrapRequest(block: suspend () -> T): T {
return try {
when (flipperTargetProviderApi.getFlipperTarget().value) {
FlipperTarget.NotConnected -> throw FlipperNotConnected()
FlipperTarget.Unsupported -> throw FirmwareNotSupported()
else -> Unit
}
if (!isDeviceConnected()) {
throw FlipperNotConnected()
}
if (!isSdCardPresent()) {
throw NoSdCardException()
}
block.invoke()
} catch (e: Throwable) {
throw e
}
}

override suspend fun getCategories(): CategoriesResponse {
return wrapRequest { api.getCategories() }
}

override suspend fun getManufacturers(categoryId: Long): BrandsResponse {
return wrapRequest { api.getManufacturers(categoryId) }
}

override suspend fun getSignal(request: SignalRequestModel): SignalResponseModel {
return wrapRequest { api.getSignal(request) }
}

override suspend fun getIfrFileContent(ifrFileId: Long): IfrFileContentResponse {
return wrapRequest { api.getIfrFileContent(ifrFileId) }
}

override suspend fun getUiFile(ifrFileId: Long): PagesLayoutBackendModel {
return wrapRequest { api.getUiFile(ifrFileId) }
}

override suspend fun getInfrareds(brandId: Long): InfraredsResponse {
return wrapRequest { api.getInfrareds(brandId) }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.flipperdevices.ifrmvp.api.infrared
package com.flipperdevices.ifrmvp.api.infrared.internal

import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.ifrmvp.api.infrared.InfraredBackendApi
import com.flipperdevices.ifrmvp.api.infrared.model.InfraredHost
import com.flipperdevices.ifrmvp.backend.model.BrandsResponse
import com.flipperdevices.ifrmvp.backend.model.CategoriesResponse
Expand Down
2 changes: 2 additions & 0 deletions components/remote-controls/brands/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ dependencies {
implementation(projects.components.core.ui.res)

implementation(projects.components.remoteControls.apiBackend)
implementation(projects.components.remoteControls.apiBackendFlipper)
implementation(projects.components.remoteControls.coreModel)
implementation(projects.components.remoteControls.coreUi)
implementation(projects.components.remoteControls.brands.api)
implementation(projects.components.faphub.errors.api)

implementation(projects.components.rootscreen.api)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.flipperdevices.remotecontrols.impl.brands.composable

import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
Expand All @@ -10,7 +11,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable
import com.flipperdevices.faphub.errors.api.FapErrorSize
import com.flipperdevices.faphub.errors.api.FapHubComposableErrorsRenderer
import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar
import com.flipperdevices.remotecontrols.impl.brands.composable.composable.BrandsLoadedContent
import com.flipperdevices.remotecontrols.impl.brands.composable.composable.BrandsLoadingComposable
Expand All @@ -20,6 +22,7 @@ import com.flipperdevices.remotecontrols.brands.impl.R as BrandsR
@Composable
fun BrandsScreen(
brandsDecomposeComponent: BrandsDecomposeComponent,
errorsRenderer: FapHubComposableErrorsRenderer,
modifier: Modifier = Modifier
) {
val coroutineScope = rememberCoroutineScope()
Expand All @@ -38,8 +41,13 @@ fun BrandsScreen(
) { scaffoldPaddings ->
Crossfade(targetState = model) { model ->
when (model) {
BrandsDecomposeComponent.Model.Error -> {
ErrorComposable(onReload = brandsDecomposeComponent::tryLoad)
is BrandsDecomposeComponent.Model.Error -> {
errorsRenderer.ComposableThrowableError(
throwable = model.throwable,
onRetry = brandsDecomposeComponent::tryLoad,
fapErrorSize = FapErrorSize.FULLSCREEN,
modifier = Modifier.fillMaxSize()
)
}

is BrandsDecomposeComponent.Model.Loaded -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package com.flipperdevices.remotecontrols.impl.brands.presentation.data

import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.ktx.jre.FlipperDispatchers
import com.flipperdevices.ifrmvp.api.infrared.InfraredBackendApi
import com.flipperdevices.ifrmvp.api.infrared.FlipperInfraredBackendApi
import com.flipperdevices.ifrmvp.backend.model.BrandModel
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.withContext
import javax.inject.Inject

@ContributesBinding(AppGraph::class, BrandsRepository::class)
class BackendBrandsRepository @Inject constructor(
private val infraredBackendApi: InfraredBackendApi,
private val infraredBackendApi: FlipperInfraredBackendApi,
) : BrandsRepository {
override suspend fun fetchBrands(
categoryId: Long
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.flipperdevices.remotecontrols.impl.brands.presentation.decompose

import com.arkivanov.decompose.ComponentContext
import com.flipperdevices.faphub.errors.api.throwable.FapHubError
import com.flipperdevices.ifrmvp.backend.model.BrandModel
import com.flipperdevices.remotecontrols.impl.brands.presentation.util.charSection
import com.flipperdevices.ui.decompose.DecomposeOnBackParameter
Expand All @@ -27,7 +28,7 @@ interface BrandsDecomposeComponent {

sealed interface Model {
data object Loading : Model
data object Error : Model
data class Error(val throwable: FapHubError) : Model
class Loaded(
val brands: ImmutableList<BrandModel>,
val query: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class BrandsDecomposeComponentImpl @AssistedInject constructor(
}

is BrandsListViewModel.State.Error -> {
BrandsDecomposeComponent.Model.Error
BrandsDecomposeComponent.Model.Error(pagingState.throwable)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import androidx.compose.runtime.Composable
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.faphub.errors.api.FapHubComposableErrorsRenderer
import com.flipperdevices.remotecontrols.api.BrandsScreenDecomposeComponent
import com.flipperdevices.remotecontrols.impl.brands.composable.BrandsScreen
import com.flipperdevices.remotecontrols.impl.brands.presentation.decompose.BrandsDecomposeComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import me.gulya.anvil.assisted.ContributesAssistedFactory

@Suppress("LongParameterList")
@ContributesAssistedFactory(AppGraph::class, BrandsScreenDecomposeComponent.Factory::class)
class BrandsScreenDecomposeComponentImpl @AssistedInject constructor(
@Assisted componentContext: ComponentContext,
Expand All @@ -19,6 +21,7 @@ class BrandsScreenDecomposeComponentImpl @AssistedInject constructor(
@Assisted onBrandClick: (brandId: Long, brandName: String) -> Unit,
@Assisted onBrandLongClick: (brandId: Long) -> Unit,
brandsDecomposeComponentFactory: BrandsDecomposeComponent.Factory,
private val errorsRenderer: FapHubComposableErrorsRenderer
) : BrandsScreenDecomposeComponent(componentContext) {
private val brandsComponent = brandsDecomposeComponentFactory.createBrandsComponent(
componentContext = childContext("BrandsComponent"),
Expand All @@ -30,6 +33,9 @@ class BrandsScreenDecomposeComponentImpl @AssistedInject constructor(

@Composable
override fun Render() {
BrandsScreen(brandsDecomposeComponent = brandsComponent)
BrandsScreen(
brandsDecomposeComponent = brandsComponent,
errorsRenderer = errorsRenderer
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.flipperdevices.remotecontrols.impl.brands.presentation.viewmodel
import com.flipperdevices.core.log.LogTagProvider
import com.flipperdevices.core.log.error
import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel
import com.flipperdevices.faphub.errors.api.throwable.FapHubError
import com.flipperdevices.faphub.errors.api.throwable.toFapHubError
import com.flipperdevices.ifrmvp.backend.model.BrandModel
import com.flipperdevices.remotecontrols.impl.brands.presentation.data.BrandsRepository
import dagger.assisted.Assisted
Expand All @@ -27,7 +29,9 @@ class BrandsListViewModel @AssistedInject constructor(
_state.update { State.Loading }
brandsRepository.fetchBrands(categoryId)
.onSuccess { _state.emit(State.Loaded(it.toImmutableList())) }
.onFailure { _state.emit(State.Error) }
.onFailure {
_state.emit(State.Error(it.toFapHubError()))
}
.onFailure { throwable -> error(throwable) { "#tryLoad could not load brands" } }
}

Expand All @@ -38,7 +42,7 @@ class BrandsListViewModel @AssistedInject constructor(
sealed interface State {
data object Loading : State
data class Loaded(val brands: ImmutableList<BrandModel>) : State
data object Error : State
data class Error(val throwable: FapHubError) : State
}

@AssistedFactory
Expand Down
2 changes: 2 additions & 0 deletions components/remote-controls/categories/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ dependencies {
implementation(projects.components.core.ui.decompose)
implementation(projects.components.core.ui.ktx)
implementation(projects.components.core.ui.res)
implementation(projects.components.faphub.errors.api)

implementation(projects.components.remoteControls.apiBackend)
implementation(projects.components.remoteControls.apiBackendFlipper)
implementation(projects.components.remoteControls.coreModel)
implementation(projects.components.remoteControls.coreUi)
implementation(projects.components.remoteControls.categories.api)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.flipperdevices.remotecontrols.impl.categories.composable

import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
Expand All @@ -9,7 +10,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.flipperdevices.core.ui.theme.LocalPalletV2
import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable
import com.flipperdevices.faphub.errors.api.FapErrorSize
import com.flipperdevices.faphub.errors.api.FapHubComposableErrorsRenderer
import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar
import com.flipperdevices.remotecontrols.impl.categories.composable.components.DeviceCategoriesLoadedContent
import com.flipperdevices.remotecontrols.impl.categories.composable.components.DeviceCategoriesLoadingContent
Expand All @@ -19,6 +21,7 @@ import com.flipperdevices.remotecontrols.categories.impl.R as CategoriesR
@Composable
internal fun DeviceCategoriesScreen(
deviceCategoriesComponent: DeviceCategoriesComponent,
errorsRenderer: FapHubComposableErrorsRenderer,
modifier: Modifier = Modifier
) {
val model by deviceCategoriesComponent.model.collectAsState()
Expand All @@ -35,8 +38,13 @@ internal fun DeviceCategoriesScreen(
) { scaffoldPaddings ->
Crossfade(model) { model ->
when (model) {
DeviceCategoriesComponent.Model.Error -> {
ErrorComposable(onReload = deviceCategoriesComponent::tryLoad)
is DeviceCategoriesComponent.Model.Error -> {
errorsRenderer.ComposableThrowableError(
throwable = model.throwable,
onRetry = deviceCategoriesComponent::tryLoad,
fapErrorSize = FapErrorSize.FULLSCREEN,
modifier = Modifier.fillMaxSize()
)
}

is DeviceCategoriesComponent.Model.Loaded -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package com.flipperdevices.remotecontrols.impl.categories.presentation.data

import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.ktx.jre.FlipperDispatchers
import com.flipperdevices.ifrmvp.api.infrared.InfraredBackendApi
import com.flipperdevices.ifrmvp.api.infrared.FlipperInfraredBackendApi
import com.flipperdevices.ifrmvp.backend.model.DeviceCategory
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.withContext
import javax.inject.Inject

@ContributesBinding(AppGraph::class, DeviceCategoriesRepository::class)
class BackendDeviceCategoriesRepository @Inject constructor(
private val infraredBackendApi: InfraredBackendApi,
private val infraredBackendApi: FlipperInfraredBackendApi,
) : DeviceCategoriesRepository {

override suspend fun fetchCategories(): Result<List<DeviceCategory>> = runCatching {
Expand Down
Loading
Loading