diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 28d6fe9135..a5dfc60808 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -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().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + StoryBookScreen(state) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index 2d2f610f44..e3318e0cab 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -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), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index ccf4b0acd0..ca2ae6a0e9 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -15,4 +15,5 @@ internal enum class TesterScreen { TEST_PUSHES, ACCOUNTS, ADDRESSES_INFO, + STORY_BOOK, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt new file mode 100644 index 0000000000..1c730da1d5 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -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, + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt new file mode 100644 index 0000000000..39d646a85a --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt @@ -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, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt new file mode 100644 index 0000000000..acea04d55b --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal fun interface StoryPageFactory { + fun create(updatePage: ((StoryBookPage) -> StoryBookPage) -> Unit): StoryBookPage +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt new file mode 100644 index 0000000000..522c3d4ae1 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt @@ -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.build(): NorthernLightsStory { + return NorthernLightsStory( + variant = NorthernLightsStory.Variant.Shader, + onVariantChange = { newVariant -> + updateStory { currentState -> + currentState.copy(variant = newVariant) + } + }, + ) +} + +internal val northernLightsStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt new file mode 100644 index 0000000000..a5764e2be5 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt @@ -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" + } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt new file mode 100644 index 0000000000..ac74e45a74 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt @@ -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(), + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt new file mode 100644 index 0000000000..73e643e0ac --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt new file mode 100644 index 0000000000..851570c925 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt @@ -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 { + fun updateStory(update: (T) -> T) +} + +internal inline fun storyPageFactory( + crossinline build: StateUpdater.() -> T, +): StoryPageFactory = StoryPageFactory { updatePage -> + val updater = object : StateUpdater { + override fun updateStory(update: (T) -> T) { + updatePage { current -> + if (current is T) update(current) else current + } + } + } + updater.build() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt new file mode 100644 index 0000000000..e08ab8c0d7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt @@ -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 = _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)) } + }, + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 9aadd20368..19d8c518ee 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -22,4 +22,5 @@ News details News details (Bottom Sheet) Addresses info + Story book