Skip to content

Commit

Permalink
Remote controls-better ui (#925)
Browse files Browse the repository at this point in the history
**Background**

This PR is designed to update update remote-control buttons so it'll be
up to date with IRDB-backend

**Changes**

- Add new ButtonData
- Add new icons
- Add new screen with infrared items by long-pressing on brand

**Test plan**

- Open remote controls
- Long press on brands to open all of it's infrareds
- Select infrareds and see new ui
  • Loading branch information
makeevrserg authored Sep 3, 2024
1 parent bc44fe2 commit 149de2e
Show file tree
Hide file tree
Showing 47 changed files with 806 additions and 311 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Attention: don't forget to add the flag for F-Droid before release
- [Feature] Remove bond on retry pair
- [Feature] Add onetap widget
- [Feature] Save, edit, share remote control
- [Feature] More UI elements for remote-controls
- [Refactor] Load RemoteControls from flipper, emulating animation
- [Refactor] Update to Kotlin 2.0
- [Refactor] Replace Ktorfit with Ktor requests in remote-controls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flipperdevices.ifrmvp.api.infrared
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
Expand All @@ -17,4 +18,6 @@ interface InfraredBackendApi {
suspend fun getIfrFileContent(ifrFileId: Long): IfrFileContentResponse

suspend fun getUiFile(ifrFileId: Long): PagesLayoutBackendModel

suspend fun getInfrareds(brandId: Long): InfraredsResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.flipperdevices.ifrmvp.api.infrared.model.InfraredHost
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
Expand Down Expand Up @@ -66,4 +67,12 @@ class InfraredBackendApiImpl(
contentType(ContentType.Application.Json)
}.body()
}

override suspend fun getInfrareds(brandId: Long): InfraredsResponse {
return httpClient.get {
url("${host.url}/infrareds")
parameter("brand_id", brandId)
contentType(ContentType.Application.Json)
}.body()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ class DeviceCategory(
val id: Long,
@SerialName("meta")
val meta: CategoryMeta,
@SerialName("folder_name")
val folderName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ data class IfrFileModel(
val id: Long,
@SerialName("brand_id")
val brandId: Long,
@SerialName("file_name")
val fileName: String,
@SerialName("folder_name")
val folderName: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.flipperdevices.ifrmvp.backend.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class InfraredsResponse(
@SerialName("infrared_files")
val infraredFiles: List<IfrFileModel>
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ abstract class BrandsScreenDecomposeComponent(
componentContext: ComponentContext,
categoryId: Long,
onBackClick: () -> Unit,
onBrandClick: (brandId: Long, brandName: String) -> Unit
onBrandClick: (brandId: Long, brandName: String) -> Unit,
onBrandLongClick: (brandId: Long) -> Unit
): BrandsScreenDecomposeComponent
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.flipperdevices.remotecontrols.api

import com.arkivanov.decompose.ComponentContext
import com.flipperdevices.ui.decompose.ScreenDecomposeComponent

abstract class InfraredsScreenDecomposeComponent(
componentContext: ComponentContext
) : ScreenDecomposeComponent(componentContext) {

interface Factory {
operator fun invoke(
componentContext: ComponentContext,
brandId: Long,
onBack: () -> Unit,
): InfraredsScreenDecomposeComponent
}
}
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 @@ -21,6 +21,8 @@ dependencies {
implementation(projects.components.remoteControls.coreUi)
implementation(projects.components.remoteControls.brands.api)

implementation(projects.components.rootscreen.api)

// Compose
implementation(libs.compose.ui)
implementation(libs.compose.tooling)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ fun BrandsScreen(
BrandsLoadedContent(
model = model,
modifier = Modifier.padding(scaffoldPaddings),
onBrandClick = brandsDecomposeComponent::onBrandClick
onBrandClick = brandsDecomposeComponent::onBrandClick,
onBrandLongClick = brandsDecomposeComponent::onBrandLongClick
)
}

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

import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.flipperdevices.ifrmvp.backend.model.IfrFileModel
import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable
import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar
import com.flipperdevices.remotecontrols.impl.brands.composable.composable.BrandsLoadingComposable
import com.flipperdevices.remotecontrols.impl.brands.composable.composable.ItemsList
import com.flipperdevices.remotecontrols.impl.brands.composable.composable.alphabet.AlphabetSearchComposable
import com.flipperdevices.remotecontrols.impl.brands.presentation.viewmodel.InfraredsListViewModel
import kotlinx.collections.immutable.toImmutableSet
import com.flipperdevices.remotecontrols.brands.impl.R as BrandsR

@Composable
fun InfraredsScreen(
viewModel: InfraredsListViewModel,
onBack: () -> Unit,
onReload: () -> Unit,
onClick: (IfrFileModel) -> Unit,
modifier: Modifier = Modifier
) {
val state by viewModel.state.collectAsState()
Scaffold(
modifier = modifier,
topBar = {
SharedTopBar(
title = stringResource(BrandsR.string.infrareds_title),
subtitle = stringResource(BrandsR.string.brands_subtitle),
onBackClick = onBack
)
}
) { scaffoldPaddings ->
Crossfade(
targetState = state,
modifier = Modifier.padding(scaffoldPaddings)
) { model ->
when (model) {
InfraredsListViewModel.State.Error -> {
ErrorComposable(onReload = onReload)
}

is InfraredsListViewModel.State.Loaded -> {
val listState = rememberLazyListState()
AlphabetSearchComposable(
items = model.infrareds,
toHeader = { it.folderName.first().uppercaseChar() },
headers = remember(model) {
model.infrareds
.map { it.fileName.first().uppercaseChar() }
.toImmutableSet()
},
listState = listState,
content = {
ItemsList(
modifier = Modifier.weight(1f),
listState = listState,
items = model.infrareds,
onClick = onClick,
toCharSection = { it.folderName.first().uppercaseChar() },
toString = { it.folderName },
onLongClick = {}
)
}
)
}

InfraredsListViewModel.State.Loading -> {
BrandsLoadingComposable()
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import androidx.compose.ui.Modifier
import com.flipperdevices.ifrmvp.backend.model.BrandModel
import com.flipperdevices.remotecontrols.impl.brands.composable.composable.alphabet.AlphabetSearchComposable
import com.flipperdevices.remotecontrols.impl.brands.presentation.decompose.BrandsDecomposeComponent
import com.flipperdevices.remotecontrols.impl.brands.presentation.util.charSection

@Composable
fun BrandsLoadedContent(
model: BrandsDecomposeComponent.Model.Loaded,
onBrandClick: (BrandModel) -> Unit,
onBrandLongClick: (BrandModel) -> Unit,
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
Expand All @@ -21,11 +23,14 @@ fun BrandsLoadedContent(
headers = model.headers,
listState = listState,
content = {
BrandsList(
ItemsList(
modifier = Modifier.weight(1f),
listState = listState,
brands = model.sortedBrands,
onBrandClick = onBrandClick
items = model.sortedBrands,
onClick = onBrandClick,
onLongClick = onBrandLongClick,
toString = { it.name },
toCharSection = { it.charSection() }
)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.flipperdevices.remotecontrols.impl.brands.composable.composable

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -22,25 +22,26 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.flipperdevices.core.ui.theme.LocalPalletV2
import com.flipperdevices.core.ui.theme.LocalTypography
import com.flipperdevices.ifrmvp.backend.model.BrandModel
import com.flipperdevices.remotecontrols.impl.brands.presentation.util.charSection
import kotlinx.collections.immutable.ImmutableList

@Composable
fun BrandsList(
brands: ImmutableList<BrandModel>,
onBrandClick: (BrandModel) -> Unit,
fun <T> ItemsList(
items: ImmutableList<T>,
onClick: (T) -> Unit,
onLongClick: (T) -> Unit,
toCharSection: (T) -> Char,
toString: (T) -> String,
modifier: Modifier = Modifier,
listState: LazyListState = rememberLazyListState(),
) {
LazyColumn(
state = listState,
modifier = modifier.padding(end = 14.dp)
) {
itemsIndexed(brands) { i, brand ->
val charSection = remember(i) { brand.charSection() }
itemsIndexed(items) { i, brand ->
val charSection = remember(i) { toCharSection.invoke(brand) }
val needDisplayTag = remember(i) {
charSection != brands.getOrNull(i - 1)?.charSection()
charSection != items.getOrNull(i - 1)?.let(toCharSection)
}
if (needDisplayTag) {
Text(
Expand All @@ -52,13 +53,16 @@ fun BrandsList(
}
Column {
Text(
text = brand.name,
text = toString.invoke(brand),
style = LocalTypography.current.bodyM14,
color = MaterialTheme.colors.onPrimary,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { onBrandClick.invoke(brand) }
.combinedClickable(
onClick = { onClick.invoke(brand) },
onLongClick = { onLongClick.invoke(brand) }
)
.padding(vertical = 12.dp)
)
Spacer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface BrandsDecomposeComponent {

fun onBrandClick(brandModel: BrandModel)

fun onBrandLongClick(brandModel: BrandModel)

fun tryLoad()

sealed interface Model {
Expand Down Expand Up @@ -51,7 +53,8 @@ interface BrandsDecomposeComponent {
componentContext: ComponentContext,
categoryId: Long,
onBackClick: DecomposeOnBackParameter,
onBrandClick: (brandId: Long, brandName: String) -> Unit
onBrandClick: (brandId: Long, brandName: String) -> Unit,
onBrandLongClick: (brandId: Long) -> Unit
): BrandsDecomposeComponent
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import kotlinx.coroutines.flow.stateIn
import me.gulya.anvil.assisted.ContributesAssistedFactory
import javax.inject.Provider

@Suppress("LongParameterList")
@ContributesAssistedFactory(AppGraph::class, BrandsDecomposeComponent.Factory::class)
class BrandsDecomposeComponentImpl @AssistedInject constructor(
@Assisted componentContext: ComponentContext,
@Assisted private val onBackClick: DecomposeOnBackParameter,
@Assisted private val onBrandClick: (brandId: Long, brandName: String) -> Unit,
@Assisted private val onBrandLongClick: (brandId: Long) -> Unit,
@Assisted categoryId: Long,
createBrandsListViewModel: BrandsListViewModel.Factory,
createQueryViewModel: Provider<QueryViewModel>
Expand Down Expand Up @@ -85,6 +87,10 @@ class BrandsDecomposeComponentImpl @AssistedInject constructor(
onBrandClick.invoke(brandModel.id, brandModel.name)
}

override fun onBrandLongClick(brandModel: BrandModel) {
onBrandLongClick.invoke(brandModel.id)
}

override fun tryLoad() {
brandsListFeature.tryLoad()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ class BrandsScreenDecomposeComponentImpl @AssistedInject constructor(
@Assisted categoryId: Long,
@Assisted onBackClick: () -> Unit,
@Assisted onBrandClick: (brandId: Long, brandName: String) -> Unit,
@Assisted onBrandLongClick: (brandId: Long) -> Unit,
brandsDecomposeComponentFactory: BrandsDecomposeComponent.Factory,
) : BrandsScreenDecomposeComponent(componentContext) {
private val brandsComponent = brandsDecomposeComponentFactory.createBrandsComponent(
componentContext = childContext("BrandsComponent"),
categoryId = categoryId,
onBackClick = onBackClick,
onBrandClick = onBrandClick
onBrandClick = onBrandClick,
onBrandLongClick = onBrandLongClick
)

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.flipperdevices.remotecontrols.impl.brands.presentation.decompose.internal

import androidx.compose.runtime.Composable
import com.arkivanov.decompose.ComponentContext
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.ui.lifecycle.viewModelWithFactory
import com.flipperdevices.remotecontrols.api.InfraredsScreenDecomposeComponent
import com.flipperdevices.remotecontrols.impl.brands.composable.InfraredsScreen
import com.flipperdevices.remotecontrols.impl.brands.presentation.viewmodel.InfraredsListViewModel
import com.flipperdevices.rootscreen.api.LocalRootNavigation
import com.flipperdevices.rootscreen.model.RootScreenConfig
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import me.gulya.anvil.assisted.ContributesAssistedFactory

@ContributesAssistedFactory(AppGraph::class, InfraredsScreenDecomposeComponent.Factory::class)
class InfraredFilesDecomposeComponentImpl @AssistedInject constructor(
@Assisted componentContext: ComponentContext,
@Assisted private val brandId: Long,
@Assisted private val onBackClick: () -> Unit,
private val infraredsListViewModelFactory: InfraredsListViewModel.Factory,
) : InfraredsScreenDecomposeComponent(componentContext) {

@Composable
override fun Render() {
val viewModel = viewModelWithFactory(null) {
infraredsListViewModelFactory.invoke(brandId)
}
val navigation = LocalRootNavigation.current
InfraredsScreen(
viewModel = viewModel,
onBack = onBackClick,
onReload = viewModel::tryLoad,
onClick = {
navigation.push(RootScreenConfig.ServerRemoteControl(it.id, it.folderName))
},
)
}
}
Loading

0 comments on commit 149de2e

Please sign in to comment.