Updated on 2026-08-14
This commit is contained in:
parent
e00fd4599e
commit
46a4a93711
24 changed files with 693 additions and 43 deletions
122
core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
Normal file
122
core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
package com.tangem.core.ui.components.chip
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Chip(state: ChipUM, modifier: Modifier = Modifier) {
|
||||||
|
val backgroundColor by animateColorAsState(
|
||||||
|
targetValue = if (state.isSelected) {
|
||||||
|
TangemTheme.colors.button.primary
|
||||||
|
} else {
|
||||||
|
TangemTheme.colors.button.secondary
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val textColor by animateColorAsState(
|
||||||
|
targetValue = if (state.isSelected) {
|
||||||
|
TangemTheme.colors.text.primary2
|
||||||
|
} else {
|
||||||
|
TangemTheme.colors.text.primary1
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(color = backgroundColor)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = ripple(),
|
||||||
|
onClick = state.onClick,
|
||||||
|
)
|
||||||
|
.padding(PaddingValues(horizontal = 24.dp, vertical = 8.dp)),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.text.resolveReference(),
|
||||||
|
style = TangemTheme.typography.button,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun ChipPreview() {
|
||||||
|
TangemThemePreview {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Chip(
|
||||||
|
state = ChipUM(
|
||||||
|
id = 0,
|
||||||
|
text = TextReference.Str("All News"),
|
||||||
|
isSelected = true,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Chip(
|
||||||
|
state = ChipUM(
|
||||||
|
id = 1,
|
||||||
|
text = TextReference.Str("Regulation"),
|
||||||
|
isSelected = false,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Chip(
|
||||||
|
state = ChipUM(
|
||||||
|
id = 2,
|
||||||
|
text = TextReference.Str("ETFs"),
|
||||||
|
isSelected = false,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Chip(
|
||||||
|
state = ChipUM(
|
||||||
|
id = 3,
|
||||||
|
text = TextReference.Str("Institutions"),
|
||||||
|
isSelected = false,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.tangem.core.ui.components.chip.entity
|
||||||
|
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
|
||||||
|
data class ChipUM(
|
||||||
|
val id: Int,
|
||||||
|
val text: TextReference,
|
||||||
|
val isSelected: Boolean = false,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -168,7 +168,9 @@ private fun LabelPreview() {
|
||||||
TangemThemePreview {
|
TangemThemePreview {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(deps.compose.foundation)
|
||||||
|
|
||||||
/* Project - Core */
|
/* Project - Core */
|
||||||
implementation(projects.core.decompose)
|
implementation(projects.core.decompose)
|
||||||
implementation(projects.core.ui)
|
implementation(projects.core.ui)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
package com.tangem.features.news.details.api
|
package com.tangem.features.news.details.api
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import com.tangem.core.decompose.factory.ComponentFactory
|
import com.tangem.core.decompose.factory.ComponentFactory
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||||
|
|
||||||
interface NewsDetailsComponent : ComposableContentComponent {
|
interface NewsDetailsComponent : ComposableContentComponent {
|
||||||
|
|
||||||
data class Params(val selectedArticleId: Int = 0)
|
data class Params(val selectedArticleId: Int = 0)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomSheetContent(
|
||||||
|
bottomSheetState: State<BottomSheetState>,
|
||||||
|
onHeaderSizeChange: (Dp) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
)
|
||||||
|
|
||||||
interface Factory : ComponentFactory<Params, NewsDetailsComponent>
|
interface Factory : ComponentFactory<Params, NewsDetailsComponent>
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package com.tangem.features.news.details.impl
|
package com.tangem.features.news.details.impl
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.tangem.core.decompose.context.AppComponentContext
|
import com.tangem.core.decompose.context.AppComponentContext
|
||||||
import com.tangem.core.decompose.model.getOrCreateModel
|
import com.tangem.core.decompose.model.getOrCreateModel
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||||
import com.tangem.features.news.details.api.NewsDetailsComponent
|
import com.tangem.features.news.details.api.NewsDetailsComponent
|
||||||
import com.tangem.features.news.details.impl.ui.NewsDetailsContent
|
import com.tangem.features.news.details.impl.ui.NewsDetailsContent
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -19,16 +23,40 @@ internal class DefaultNewsDetailsComponent @AssistedInject constructor(
|
||||||
|
|
||||||
private val model: NewsDetailsModel = getOrCreateModel(params)
|
private val model: NewsDetailsModel = getOrCreateModel(params)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun BottomSheetContent(
|
||||||
|
bottomSheetState: State<BottomSheetState>,
|
||||||
|
onHeaderSizeChange: (Dp) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||||
|
val bsState by bottomSheetState
|
||||||
|
|
||||||
|
BackHandler(enabled = bsState == BottomSheetState.EXPANDED) {
|
||||||
|
navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
NewsDetailsContent(
|
||||||
|
state = uiState,
|
||||||
|
onBackClick = ::navigateBack,
|
||||||
|
modifier = modifier,
|
||||||
|
isBottomSheetMode = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content(modifier: Modifier) {
|
override fun Content(modifier: Modifier) {
|
||||||
val uiState by model.uiState.collectAsState()
|
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||||
NewsDetailsContent(
|
NewsDetailsContent(
|
||||||
state = uiState,
|
state = uiState,
|
||||||
onBackClick = model::onBackClick,
|
onBackClick = model::onBackClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
isBottomSheetMode = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun navigateBack() = router.pop()
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : NewsDetailsComponent.Factory {
|
interface Factory : NewsDetailsComponent.Factory {
|
||||||
override fun create(
|
override fun create(
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,19 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||||
import com.tangem.core.ui.components.pager.PagerIndicator
|
import com.tangem.core.ui.components.pager.PagerIndicator
|
||||||
|
import com.tangem.core.ui.extensions.conditionalCompose
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import com.tangem.features.news.details.impl.MockArticlesFactory
|
import com.tangem.features.news.details.impl.MockArticlesFactory
|
||||||
|
|
||||||
// TODO [REDACTED_TASK_KEY] make internal
|
// TODO [REDACTED_TASK_KEY] make internal
|
||||||
@Composable
|
@Composable
|
||||||
fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
fun NewsDetailsContent(
|
||||||
|
state: NewsDetailsUM,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isBottomSheetMode: Boolean = false,
|
||||||
|
) {
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
initialPage = state.selectedArticleIndex,
|
initialPage = state.selectedArticleIndex,
|
||||||
pageCount = { state.articles.size },
|
pageCount = { state.articles.size },
|
||||||
|
|
@ -39,10 +45,9 @@ fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier:
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(TangemTheme.colors.background.secondary)
|
.background(TangemTheme.colors.background.tertiary)
|
||||||
.systemBarsPadding(),
|
.conditionalCompose(isBottomSheetMode) { systemBarsPadding() },
|
||||||
) {
|
) {
|
||||||
Column {
|
|
||||||
TangemTopAppBar(
|
TangemTopAppBar(
|
||||||
title = null,
|
title = null,
|
||||||
startButton = TopAppBarButtonUM.Icon(
|
startButton = TopAppBarButtonUM.Icon(
|
||||||
|
|
@ -86,7 +91,6 @@ fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
|
|
|
||||||
1
features/news/news-list/api/.gitignore
vendored
Normal file
1
features/news/news-list/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
20
features/news/news-list/api/build.gradle.kts
Normal file
20
features/news/news-list/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
plugins {
|
||||||
|
alias(deps.plugins.android.library)
|
||||||
|
alias(deps.plugins.kotlin.android)
|
||||||
|
id("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.tangem.features.news.list.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(deps.compose.foundation)
|
||||||
|
|
||||||
|
/* Project - Core */
|
||||||
|
implementation(projects.core.decompose)
|
||||||
|
implementation(projects.core.ui)
|
||||||
|
|
||||||
|
/* Compose */
|
||||||
|
implementation(deps.compose.runtime)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.tangem.features.news.list.api
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import com.tangem.core.decompose.factory.ComponentFactory
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||||
|
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||||
|
|
||||||
|
interface NewsListComponent : ComposableContentComponent {
|
||||||
|
|
||||||
|
data class Params(val selectedFilter: String? = null)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomSheetContent(
|
||||||
|
bottomSheetState: State<BottomSheetState>,
|
||||||
|
onHeaderSizeChange: (Dp) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Factory : ComponentFactory<Params, NewsListComponent>
|
||||||
|
}
|
||||||
1
features/news/news-list/impl/.gitignore
vendored
Normal file
1
features/news/news-list/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
46
features/news/news-list/impl/build.gradle.kts
Normal file
46
features/news/news-list/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
plugins {
|
||||||
|
alias(deps.plugins.android.library)
|
||||||
|
alias(deps.plugins.kotlin.android)
|
||||||
|
alias(deps.plugins.kotlin.kapt)
|
||||||
|
alias(deps.plugins.hilt.android)
|
||||||
|
id("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.tangem.features.news.list.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
/* AndroidX */
|
||||||
|
implementation(deps.lifecycle.compose)
|
||||||
|
implementation(deps.androidx.activity.compose)
|
||||||
|
|
||||||
|
/** Compose */
|
||||||
|
implementation(deps.compose.foundation)
|
||||||
|
implementation(deps.compose.ui)
|
||||||
|
implementation(deps.compose.ui.tooling)
|
||||||
|
implementation(deps.compose.material3)
|
||||||
|
|
||||||
|
/** Core modules */
|
||||||
|
implementation(projects.core.ui)
|
||||||
|
implementation(projects.core.utils)
|
||||||
|
implementation(projects.core.decompose)
|
||||||
|
implementation(projects.common.ui)
|
||||||
|
implementation(projects.common.routing)
|
||||||
|
|
||||||
|
/** Feature modules */
|
||||||
|
implementation(projects.features.news.newsList.api)
|
||||||
|
|
||||||
|
/** Domain modules */
|
||||||
|
implementation(projects.domain.models)
|
||||||
|
implementation(projects.domain.news)
|
||||||
|
|
||||||
|
/** Other dependencies */
|
||||||
|
implementation(deps.kotlin.immutable.collections)
|
||||||
|
implementation(deps.arrow.core)
|
||||||
|
implementation(deps.timber)
|
||||||
|
|
||||||
|
/** DI */
|
||||||
|
implementation(deps.hilt.android)
|
||||||
|
kapt(deps.hilt.kapt)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.tangem.features.news.list.impl
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.tangem.core.decompose.context.AppComponentContext
|
||||||
|
import com.tangem.core.decompose.model.getOrCreateModel
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||||
|
import com.tangem.features.news.list.api.NewsListComponent
|
||||||
|
import com.tangem.features.news.list.impl.ui.NewsListContent
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
|
||||||
|
internal class DefaultNewsListComponent @AssistedInject constructor(
|
||||||
|
@Assisted context: AppComponentContext,
|
||||||
|
@Assisted params: NewsListComponent.Params,
|
||||||
|
) : NewsListComponent, AppComponentContext by context {
|
||||||
|
|
||||||
|
private val model: NewsListModel = getOrCreateModel(params)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun BottomSheetContent(
|
||||||
|
bottomSheetState: State<BottomSheetState>,
|
||||||
|
onHeaderSizeChange: (Dp) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||||
|
val bsState by bottomSheetState
|
||||||
|
|
||||||
|
BackHandler(enabled = bsState == BottomSheetState.EXPANDED) {
|
||||||
|
navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
NewsListContent(
|
||||||
|
state = uiState,
|
||||||
|
onBackClick = ::navigateBack,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content(modifier: Modifier) {
|
||||||
|
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||||
|
NewsListContent(
|
||||||
|
state = uiState,
|
||||||
|
onBackClick = model::onBackClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateBack() = router.pop()
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : NewsListComponent.Factory {
|
||||||
|
override fun create(context: AppComponentContext, params: NewsListComponent.Params): DefaultNewsListComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.tangem.features.news.list.impl
|
||||||
|
|
||||||
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
|
import com.tangem.core.decompose.model.Model
|
||||||
|
import com.tangem.core.decompose.navigation.Router
|
||||||
|
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
|
||||||
|
import com.tangem.features.news.list.impl.ui.NewsListUM
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ModelScoped
|
||||||
|
internal class NewsListModel @Inject constructor(
|
||||||
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
private val getNewsCategoriesUseCase: GetNewsCategoriesUseCase,
|
||||||
|
private val router: Router,
|
||||||
|
) : Model() {
|
||||||
|
|
||||||
|
val uiState: StateFlow<NewsListUM>
|
||||||
|
field = MutableStateFlow(
|
||||||
|
NewsListUM(
|
||||||
|
selectedCategoryId = 0,
|
||||||
|
filters = persistentListOf(),
|
||||||
|
articles = persistentListOf(),
|
||||||
|
onArticleClick = ::onArticleClick,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
modelScope.launch(dispatchers.default) {
|
||||||
|
val filterChips = getNewsCategoriesUseCase
|
||||||
|
.invoke()
|
||||||
|
.map { articleCategory ->
|
||||||
|
ChipUM(
|
||||||
|
id = articleCategory.id,
|
||||||
|
text = TextReference.Str(articleCategory.name),
|
||||||
|
isSelected = false,
|
||||||
|
onClick = {
|
||||||
|
onCategoryClick(articleCategory.id)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toImmutableList()
|
||||||
|
uiState.update { currentState ->
|
||||||
|
currentState.copy(filters = filterChips)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackClick() {
|
||||||
|
router.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onArticleClick(articleId: Int) {
|
||||||
|
// TODO [REDACTED_TASK_KEY]
|
||||||
|
articleId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCategoryClick(categoryId: Int) {
|
||||||
|
uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
selectedCategoryId = categoryId,
|
||||||
|
filters = updateFilterChips(categoryId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFilterChips(categoryId: Int): ImmutableList<ChipUM> {
|
||||||
|
return uiState.value.filters.map { chip ->
|
||||||
|
chip.copy(isSelected = chip.id == categoryId)
|
||||||
|
}.toImmutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.tangem.features.news.list.impl.di
|
||||||
|
|
||||||
|
import com.tangem.features.news.list.api.NewsListComponent
|
||||||
|
import com.tangem.features.news.list.impl.DefaultNewsListComponent
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal interface NewsListModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindNewsListComponentFactory(factory: DefaultNewsListComponent.Factory): NewsListComponent.Factory
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
package com.tangem.features.news.list.impl.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.common.ui.news.ArticleCard
|
||||||
|
import com.tangem.common.ui.news.ArticleConfigUM
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||||
|
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||||
|
import com.tangem.core.ui.components.chip.Chip
|
||||||
|
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||||
|
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun NewsListContent(state: NewsListUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color = TangemTheme.colors.background.tertiary),
|
||||||
|
) {
|
||||||
|
AppBarWithBackButton(
|
||||||
|
text = stringResourceSafe(R.string.common_news),
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = state.filters,
|
||||||
|
key = { it.id },
|
||||||
|
) { filter ->
|
||||||
|
Chip(state = filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = state.articles,
|
||||||
|
key = ArticleConfigUM::id,
|
||||||
|
) { article ->
|
||||||
|
ArticleCard(
|
||||||
|
articleConfigUM = article,
|
||||||
|
onArticleClick = { state.onArticleClick(article.id) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(164.dp),
|
||||||
|
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||||
|
)
|
||||||
|
SpacerH(12.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun NewsListContentPreview() {
|
||||||
|
val tags = listOf(
|
||||||
|
LabelUM(TextReference.Str("Regulation")),
|
||||||
|
LabelUM(TextReference.Str("BTC")),
|
||||||
|
LabelUM(TextReference.Str("ETH")),
|
||||||
|
).toImmutableSet()
|
||||||
|
|
||||||
|
val articles = persistentListOf(
|
||||||
|
ArticleConfigUM(
|
||||||
|
id = 1,
|
||||||
|
title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds",
|
||||||
|
score = 6.5f,
|
||||||
|
createdAt = TextReference.Str("1h ago"),
|
||||||
|
isTrending = false,
|
||||||
|
tags = tags,
|
||||||
|
isViewed = false,
|
||||||
|
),
|
||||||
|
ArticleConfigUM(
|
||||||
|
id = 2,
|
||||||
|
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||||
|
score = 8.6f,
|
||||||
|
createdAt = TextReference.Str("8h ago"),
|
||||||
|
isTrending = false,
|
||||||
|
tags = tags,
|
||||||
|
isViewed = true,
|
||||||
|
),
|
||||||
|
ArticleConfigUM(
|
||||||
|
id = 3,
|
||||||
|
title = "Bitcoin reclaims ~\$115K amid macro prints and ETF optimism",
|
||||||
|
score = 7.8f,
|
||||||
|
createdAt = TextReference.Str("22 Jun, 11:30"),
|
||||||
|
isTrending = false,
|
||||||
|
tags = tags,
|
||||||
|
isViewed = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val filters = persistentListOf(
|
||||||
|
ChipUM(
|
||||||
|
id = 0,
|
||||||
|
text = TextReference.Str("All News"),
|
||||||
|
isSelected = true,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
ChipUM(
|
||||||
|
id = 1,
|
||||||
|
text = TextReference.Str("Regulation"),
|
||||||
|
isSelected = false,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
ChipUM(
|
||||||
|
id = 2,
|
||||||
|
text = TextReference.Str("ETFs"),
|
||||||
|
isSelected = false,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
ChipUM(
|
||||||
|
id = 3,
|
||||||
|
text = TextReference.Str("Institutions"),
|
||||||
|
isSelected = false,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
TangemThemePreview {
|
||||||
|
NewsListContent(
|
||||||
|
state = NewsListUM(
|
||||||
|
selectedCategoryId = 0,
|
||||||
|
filters = filters,
|
||||||
|
articles = articles,
|
||||||
|
onArticleClick = {},
|
||||||
|
),
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.tangem.features.news.list.impl.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.tangem.common.ui.news.ArticleConfigUM
|
||||||
|
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class NewsListUM(
|
||||||
|
val selectedCategoryId: Int?,
|
||||||
|
val filters: ImmutableList<ChipUM>,
|
||||||
|
val articles: ImmutableList<ArticleConfigUM>,
|
||||||
|
val onArticleClick: (Int) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -61,6 +61,8 @@ dependencies {
|
||||||
implementation(projects.features.pushNotifications.api)
|
implementation(projects.features.pushNotifications.api)
|
||||||
implementation(projects.features.news.newsDetails.api)
|
implementation(projects.features.news.newsDetails.api)
|
||||||
implementation(projects.features.news.newsDetails.impl)
|
implementation(projects.features.news.newsDetails.impl)
|
||||||
|
implementation(projects.features.news.newsList.api)
|
||||||
|
implementation(projects.features.news.newsList.impl)
|
||||||
|
|
||||||
/* SDK */
|
/* SDK */
|
||||||
implementation(tangemDeps.blockchain)
|
implementation(tangemDeps.blockchain)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import com.tangem.common.routing.AppRouter
|
import com.tangem.common.routing.AppRouter
|
||||||
import com.tangem.core.navigation.finisher.AppFinisher
|
import com.tangem.core.navigation.finisher.AppFinisher
|
||||||
import com.tangem.core.ui.UiDependencies
|
import com.tangem.core.ui.UiDependencies
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.screen.ComposeActivity
|
import com.tangem.core.ui.screen.ComposeActivity
|
||||||
import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen
|
import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen
|
||||||
|
|
@ -38,9 +41,11 @@ import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewMod
|
||||||
import com.tangem.feature.tester.presentation.news.ui.NewsScreen
|
import com.tangem.feature.tester.presentation.news.ui.NewsScreen
|
||||||
import com.tangem.feature.tester.presentation.news.viewmodel.NewsViewModel
|
import com.tangem.feature.tester.presentation.news.viewmodel.NewsViewModel
|
||||||
import com.tangem.features.news.details.impl.MockArticlesFactory
|
import com.tangem.features.news.details.impl.MockArticlesFactory
|
||||||
|
import com.tangem.features.news.details.impl.ui.ArticleUM
|
||||||
import com.tangem.features.news.details.impl.ui.NewsDetailsContent
|
import com.tangem.features.news.details.impl.ui.NewsDetailsContent
|
||||||
import com.tangem.features.news.details.impl.ui.NewsDetailsUM
|
import com.tangem.features.news.details.impl.ui.NewsDetailsUM
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -190,6 +195,43 @@ internal class TesterActivity : ComposeActivity() {
|
||||||
onBackClick = { innerTesterRouter.back() },
|
onBackClick = { innerTesterRouter.back() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(route = TesterScreen.NEWS_DETAILS_BOTTOM_SHEET.name) {
|
||||||
|
NewsDetailsBottomSheetTest(
|
||||||
|
onDismiss = { innerTesterRouter.back() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NewsDetailsBottomSheetTest(onDismiss: () -> Unit) {
|
||||||
|
data class NewsDetailsBottomSheetContent(
|
||||||
|
val articles: ImmutableList<ArticleUM>,
|
||||||
|
) : TangemBottomSheetConfigContent
|
||||||
|
|
||||||
|
val content = NewsDetailsBottomSheetContent(
|
||||||
|
articles = MockArticlesFactory.createMockArticles(),
|
||||||
|
)
|
||||||
|
|
||||||
|
TangemBottomSheet<NewsDetailsBottomSheetContent>(
|
||||||
|
config = TangemBottomSheetConfig(
|
||||||
|
isShown = true,
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
content = content,
|
||||||
|
),
|
||||||
|
containerColor = TangemTheme.colors.background.tertiary,
|
||||||
|
) { sheetContent ->
|
||||||
|
NewsDetailsContent(
|
||||||
|
state = NewsDetailsUM(
|
||||||
|
articles = sheetContent.articles,
|
||||||
|
selectedArticleIndex = 0,
|
||||||
|
onLikeClick = { },
|
||||||
|
onShareClick = { },
|
||||||
|
),
|
||||||
|
onBackClick = onDismiss,
|
||||||
|
isBottomSheetMode = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,4 +16,5 @@ internal enum class TesterScreen {
|
||||||
ACCOUNTS,
|
ACCOUNTS,
|
||||||
NEWS,
|
NEWS,
|
||||||
NEWS_DETAILS,
|
NEWS_DETAILS,
|
||||||
|
NEWS_DETAILS_BOTTOM_SHEET,
|
||||||
}
|
}
|
||||||
|
|
@ -12,5 +12,6 @@ data class NewsUM(
|
||||||
|
|
||||||
enum class ButtonUM(@StringRes val textResId: Int) {
|
enum class ButtonUM(@StringRes val textResId: Int) {
|
||||||
NEWS_DETAILS(R.string.news_details),
|
NEWS_DETAILS(R.string.news_details),
|
||||||
|
NEWS_DETAILS_BOTTOM_SHEET(R.string.news_details_bottom_sheet),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ internal class NewsViewModel @Inject constructor() : ViewModel() {
|
||||||
onBackClick = ::onBackClick,
|
onBackClick = ::onBackClick,
|
||||||
buttons = persistentSetOf(
|
buttons = persistentSetOf(
|
||||||
NewsUM.ButtonUM.NEWS_DETAILS,
|
NewsUM.ButtonUM.NEWS_DETAILS,
|
||||||
|
NewsUM.ButtonUM.NEWS_DETAILS_BOTTOM_SHEET,
|
||||||
),
|
),
|
||||||
onButtonClick = ::onButtonClick,
|
onButtonClick = ::onButtonClick,
|
||||||
)
|
)
|
||||||
|
|
@ -39,6 +40,7 @@ internal class NewsViewModel @Inject constructor() : ViewModel() {
|
||||||
private fun onButtonClick(button: NewsUM.ButtonUM) {
|
private fun onButtonClick(button: NewsUM.ButtonUM) {
|
||||||
when (button) {
|
when (button) {
|
||||||
NewsUM.ButtonUM.NEWS_DETAILS -> router?.open(TesterScreen.NEWS_DETAILS)
|
NewsUM.ButtonUM.NEWS_DETAILS -> router?.open(TesterScreen.NEWS_DETAILS)
|
||||||
|
NewsUM.ButtonUM.NEWS_DETAILS_BOTTOM_SHEET -> router?.open(TesterScreen.NEWS_DETAILS_BOTTOM_SHEET)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20,4 +20,5 @@
|
||||||
<string name="accounts" translatable="false">Accounts</string>
|
<string name="accounts" translatable="false">Accounts</string>
|
||||||
<string name="news" translatable="false">News</string>
|
<string name="news" translatable="false">News</string>
|
||||||
<string name="news_details" translatable="false">News details</string>
|
<string name="news_details" translatable="false">News details</string>
|
||||||
|
<string name="news_details_bottom_sheet" translatable="false">News details (Bottom Sheet)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,9 @@ include(":features:feed:impl")
|
||||||
|
|
||||||
include(":features:news:news-details:api")
|
include(":features:news:news-details:api")
|
||||||
include(":features:news:news-details:impl")
|
include(":features:news:news-details:impl")
|
||||||
|
|
||||||
|
include(":features:news:news-list:api")
|
||||||
|
include(":features:news:news-list:impl")
|
||||||
// endregion Feature modules
|
// endregion Feature modules
|
||||||
|
|
||||||
// region Domain modules
|
// region Domain modules
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue