Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-24 19:38:10 +03:00
parent febd32b964
commit 973109c974
13 changed files with 299 additions and 0 deletions

View file

@ -35,6 +35,8 @@ import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.feature.tester.presentation.navigation.TesterScreen
import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersScreen
import com.tangem.feature.tester.presentation.storybook.ui.StoryBookScreen
import com.tangem.feature.tester.presentation.storybook.viewmodel.StoryBookViewModel
import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel
import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen
import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel
@ -86,6 +88,7 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.TEST_PUSHES,
ButtonUM.ACCOUNTS,
ButtonUM.ADDRESSES_INFO,
ButtonUM.STORY_BOOK,
),
onButtonClick = { buttonUM ->
val route = when (buttonUM) {
@ -97,6 +100,7 @@ internal class TesterActivity : ComposeActivity() {
ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES
ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS
ButtonUM.ADDRESSES_INFO -> TesterScreen.ADDRESSES_INFO
ButtonUM.STORY_BOOK -> TesterScreen.STORY_BOOK
}
innerTesterRouter.open(route)
@ -179,6 +183,15 @@ internal class TesterActivity : ComposeActivity() {
AddressesInfoScreen(state)
}
composable(route = TesterScreen.STORY_BOOK.name) {
val viewModel = hiltViewModel<StoryBookViewModel>().apply {
setupNavigation(innerTesterRouter)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
StoryBookScreen(state)
}
}
}
}

View file

@ -26,5 +26,6 @@ data class TesterMenuUM(
TEST_PUSHES(R.string.test_push),
ACCOUNTS(R.string.accounts),
ADDRESSES_INFO(R.string.addresses_info),
STORY_BOOK(R.string.story_book),
}
}

View file

@ -15,4 +15,5 @@ internal enum class TesterScreen {
TEST_PUSHES,
ACCOUNTS,
ADDRESSES_INFO,
STORY_BOOK,
}

View file

@ -0,0 +1,15 @@
package com.tangem.feature.tester.presentation.storybook.entity
internal sealed interface StoryBookPage
internal data object StoryList : StoryBookPage
internal data class NorthernLightsStory(
val variant: Variant,
val onVariantChange: (Variant) -> Unit,
) : StoryBookPage {
enum class Variant {
Shader,
Simple,
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.feature.tester.presentation.storybook.entity
internal data class StoryBookUM(
val currentPage: StoryBookPage = StoryList,
val onBackClick: () -> Unit,
val onStoryClick: (StoryPageFactory) -> Unit,
)

View file

@ -0,0 +1,5 @@
package com.tangem.feature.tester.presentation.storybook.entity
internal fun interface StoryPageFactory {
fun create(updatePage: ((StoryBookPage) -> StoryBookPage) -> Unit): StoryBookPage
}

View file

@ -0,0 +1,19 @@
package com.tangem.feature.tester.presentation.storybook.page.background
import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
internal fun StateUpdater<NorthernLightsStory>.build(): NorthernLightsStory {
return NorthernLightsStory(
variant = NorthernLightsStory.Variant.Shader,
onVariantChange = { newVariant ->
updateStory { currentState ->
currentState.copy(variant = newVariant)
}
},
)
}
internal val northernLightsStoryFactory
get() = storyPageFactory(StateUpdater<NorthernLightsStory>::build)

View file

@ -0,0 +1,89 @@
@file:Suppress("MagicNumber")
package com.tangem.feature.tester.presentation.storybook.page.background
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground
import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory
@Composable
internal fun NorthernLightsStory(state: NorthernLightsStory, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize()) {
NorthernLightsBackground(
modifier = Modifier.fillMaxSize(),
forceSimpleVersion = state.variant == NorthernLightsStory.Variant.Simple,
)
NorthernLightsVariantToggle(
selected = state.variant,
onSelect = state.onVariantChange,
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(bottom = 24.dp)
.padding(horizontal = 24.dp),
)
}
}
@Composable
private fun NorthernLightsVariantToggle(
selected: NorthernLightsStory.Variant,
onSelect: (NorthernLightsStory.Variant) -> Unit,
modifier: Modifier = Modifier,
) {
val shape = RoundedCornerShape(50)
Row(
modifier = modifier
.clip(shape)
.background(Color.Black.copy(alpha = 0.35f))
.border(width = 1.dp, color = Color.White.copy(alpha = 0.15f), shape = shape)
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
NorthernLightsStory.Variant.entries.forEach { variant ->
VariantChip(
label = variant.label,
selected = variant == selected,
onClick = { onSelect(variant) },
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun VariantChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val chipShape = RoundedCornerShape(50)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.clip(chipShape)
.background(if (selected) Color.White.copy(alpha = 0.2f) else Color.Transparent)
.clickable(onClick = onClick)
.padding(vertical = 10.dp, horizontal = 16.dp),
) {
Text(
text = label,
color = Color.White,
fontSize = 14.sp,
)
}
}
private val NorthernLightsStory.Variant.label: String
get() = when (this) {
NorthernLightsStory.Variant.Shader -> "Shader"
NorthernLightsStory.Variant.Simple -> "Simple"
}

View file

@ -0,0 +1,53 @@
package com.tangem.feature.tester.presentation.storybook.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM
import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory
import com.tangem.feature.tester.presentation.storybook.page.background.northernLightsStoryFactory
private data class StoryItem(val title: String, val factory: StoryPageFactory)
private fun buildStories() = listOf(
StoryItem(title = "Northern Lights Background", factory = northernLightsStoryFactory),
)
@Composable
internal fun StoryBookListScreen(state: StoryBookUM, modifier: Modifier = Modifier) {
val stories = remember { buildStories() }
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
) {
stickyHeader {
AppBarWithBackButton(
onBackClick = state.onBackClick,
text = "Storybook",
containerColor = TangemTheme.colors.background.primary,
)
}
items(items = stories, key = { it.title }) { item ->
PrimaryButton(
text = item.title,
onClick = { state.onStoryClick(item.factory) },
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
)
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.feature.tester.presentation.storybook.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory
import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM
import com.tangem.feature.tester.presentation.storybook.entity.StoryList
import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory
@Composable
internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
AnimatedContent(
targetState = state.currentPage,
modifier = modifier,
) { storyState ->
when (storyState) {
StoryList -> StoryBookListScreen(state = state)
is NorthernLightsStory -> NorthernLightsStory(state = storyState)
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.feature.tester.presentation.storybook.viewmodel
import com.tangem.feature.tester.presentation.storybook.entity.StoryBookPage
import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory
internal interface StateUpdater<T> {
fun updateStory(update: (T) -> T)
}
internal inline fun <reified T : StoryBookPage> storyPageFactory(
crossinline build: StateUpdater<T>.() -> T,
): StoryPageFactory = StoryPageFactory { updatePage ->
val updater = object : StateUpdater<T> {
override fun updateStory(update: (T) -> T) {
updatePage { current ->
if (current is T) update(current) else current
}
}
}
updater.build()
}

View file

@ -0,0 +1,49 @@
package com.tangem.feature.tester.presentation.storybook.viewmodel
import androidx.lifecycle.ViewModel
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM
import com.tangem.feature.tester.presentation.storybook.entity.StoryList
import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@HiltViewModel
internal class StoryBookViewModel @Inject constructor() : ViewModel() {
private var router: InnerTesterRouter? = null
private val _uiState = MutableStateFlow(
StoryBookUM(
onBackClick = ::onBackClick,
onStoryClick = ::onStoryClick,
),
)
val uiState: StateFlow<StoryBookUM> = _uiState.asStateFlow()
fun setupNavigation(router: InnerTesterRouter) {
this.router = router
}
private fun onBackClick() {
if (_uiState.value.currentPage !is StoryList) {
_uiState.update { it.copy(currentPage = StoryList) }
} else {
router?.back()
}
}
private fun onStoryClick(factory: StoryPageFactory) {
_uiState.update { state ->
state.copy(
currentPage = factory.create { update ->
_uiState.update { s -> s.copy(currentPage = update(s.currentPage)) }
},
)
}
}
}

View file

@ -22,4 +22,5 @@
<string name="news_details" translatable="false">News details</string>
<string name="news_details_bottom_sheet" translatable="false">News details (Bottom Sheet)</string>
<string name="addresses_info" translatable="false">Addresses info</string>
<string name="story_book" translatable="false">Story book</string>
</resources>