diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/PagerTabs.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/PagerTabs.kt new file mode 100644 index 00000000..f211cefd --- /dev/null +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/PagerTabs.kt @@ -0,0 +1,129 @@ +@file:OptIn(ExperimentalFoundationApi::class) + +package co.electriccoin.zcash.ui.design.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Preview +@Composable +private fun PagerTabsPreview() { + ZcashTheme(forceDarkMode = false) { + BlankSurface { + PagerTabs( + pagerState = rememberPagerState { 2 }, + tabs = persistentListOf("First", "Second"), + ) + } + } +} + +@Preview +@Composable +private fun PagerTabsDarkPreview() { + ZcashTheme(forceDarkMode = true) { + BlankSurface { + PagerTabs( + pagerState = rememberPagerState { 2 }, + tabs = persistentListOf("First", "Second"), + ) + } + } +} + +@Composable +fun PagerTabs( + pagerState: PagerState, + tabs: ImmutableList, + modifier: Modifier = Modifier, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + onTabSelected: (index: Int) -> Unit = {}, +) { + TabRow( + modifier = + modifier + .padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingBig) + .border(ZcashTheme.dimens.spacingTiny, ZcashTheme.colors.layoutStroke), + selectedTabIndex = pagerState.currentPage, + divider = {}, + indicator = {}, + ) { + tabs.forEachIndexed { index, tab -> + PagerTab( + title = tab, + selected = pagerState.currentPage == index, + onClick = { + coroutineScope.launch { + onTabSelected(index) + pagerState.animateScrollToPage(index) + } + }, + ) + } + } +} + +@Composable +private fun PagerTab( + title: String, + selected: Boolean, + onClick: () -> Unit, +) { + Tab( + modifier = + Modifier + .fillMaxWidth(), + selected = selected, + onClick = onClick, + selectedContentColor = Color.Transparent, + unselectedContentColor = ZcashTheme.colors.layoutStroke + ) { + Box( + modifier = + Modifier + .fillMaxSize() + .background( + if (selected) Color.Transparent else ZcashTheme.colors.layoutStroke + ) + .padding(vertical = ZcashTheme.dimens.spacingMid, horizontal = ZcashTheme.dimens.spacingXtiny), + contentAlignment = Alignment.Center, + ) { + Text( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = ZcashTheme.dimens.spacingXtiny), + text = title, + color = if (selected) ZcashTheme.colors.textCommon else MaterialTheme.colorScheme.onPrimary, + style = ZcashTheme.extendedTypography.restoringTopAppBarStyle, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt index 74fc0bce..648af646 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt @@ -1,16 +1,20 @@ package co.electriccoin.zcash.ui.screen.receive.view +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon @@ -19,6 +23,8 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -44,34 +50,17 @@ import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.test.CommonTag import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator +import co.electriccoin.zcash.ui.design.component.PagerTabs import co.electriccoin.zcash.ui.design.component.Reference import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.SubHeader import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.runBlocking import kotlin.math.roundToInt -@Preview("Receive") -@Composable -private fun ComposablePreview() { - ZcashTheme(forceDarkMode = false) { - Receive( - screenBrightnessState = ScreenBrightnessState.NORMAL, - walletAddress = runBlocking { WalletAddressesFixture.new() }, - snackbarHostState = SnackbarHostState(), - onSettings = {}, - onAdjustBrightness = {}, - onAddrCopyToClipboard = {}, - onQrImageShare = {}, - topAppBarSubTitleState = TopAppBarSubTitleState.None, - versionInfo = VersionInfoFixture.new(), - ) - } -} - @Suppress("LongParameterList") @Composable fun Receive( @@ -102,18 +91,16 @@ fun Receive( CircularScreenProgressIndicator() } else { ReceiveContents( - walletAddress = walletAddress, + walletAddresses = walletAddress, onAddressCopyToClipboard = onAddrCopyToClipboard, onQrImageShare = onQrImageShare, screenBrightnessState = screenBrightnessState, versionInfo = versionInfo, modifier = Modifier.padding( - top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault, - bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault, - start = ZcashTheme.dimens.screenHorizontalSpacingRegular, - end = ZcashTheme.dimens.screenHorizontalSpacingRegular - ) + top = paddingValues.calculateTopPadding() + // We intentionally do not set the rest paddings, those are set by the underlying composable + ), ) } } @@ -160,154 +147,173 @@ private fun ReceiveTopAppBar( ) } +@OptIn(ExperimentalFoundationApi::class) @Suppress("LongParameterList") @Composable private fun ReceiveContents( - walletAddress: WalletAddresses, + walletAddresses: WalletAddresses, onAddressCopyToClipboard: (String) -> Unit, onQrImageShare: (ImageBitmap) -> Unit, screenBrightnessState: ScreenBrightnessState, versionInfo: VersionInfo, modifier: Modifier = Modifier, ) { - Column( - modifier = - Modifier - .fillMaxHeight() - .verticalScroll(rememberScrollState()) - .then(modifier), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (screenBrightnessState == ScreenBrightnessState.FULL) { - BrightenScreen() - DisableScreenTimeout() - } + if (screenBrightnessState == ScreenBrightnessState.FULL) { + BrightenScreen() + DisableScreenTimeout() + } - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) - - Address( - walletAddress = walletAddress.unified, - onAddressCopyToClipboard = onAddressCopyToClipboard, - onQrImageShare = onQrImageShare, - ) - - if (versionInfo.isTestnet) { - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge)) - - Address( - walletAddress = walletAddress.sapling, - onAddressCopyToClipboard = onAddressCopyToClipboard, - onQrImageShare = onQrImageShare, + val state by remember { + derivedStateOf { + listOfNotNull( + walletAddresses.unified, + walletAddresses.transparent, ) } - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge)) - - Address( - walletAddress = walletAddress.transparent, - onAddressCopyToClipboard = onAddressCopyToClipboard, - onQrImageShare = onQrImageShare, - ) } -} -private val DEFAULT_QR_CODE_SIZE = 320.dp + val pagerState = rememberPagerState { state.size } -@Suppress("LongMethod") -@Composable -private fun Address( - walletAddress: WalletAddress, - onAddressCopyToClipboard: (String) -> Unit, - onQrImageShare: (ImageBitmap) -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - SubHeader( - text = - stringResource( - id = - when (walletAddress) { + Column( + modifier = modifier, + ) { + Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) + + PagerTabs( + modifier = Modifier.fillMaxWidth(), + pagerState = pagerState, + tabs = + state.map { + stringResource( + when (it) { is WalletAddress.Unified -> R.string.receive_wallet_address_unified is WalletAddress.Sapling -> R.string.receive_wallet_address_sapling is WalletAddress.Transparent -> R.string.receive_wallet_address_transparent } - ), - modifier = Modifier.align(Alignment.CenterHorizontally) + ) + }.toPersistentList(), ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny)) - - val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt() - val qrCodeImage = - remember { - qrCodeForAddress( - address = walletAddress.address, - size = sizePixels - ) - } - - QrCode( - qrCodeImage = qrCodeImage, - onQrImageBitmapShare = onQrImageShare, - contentDescription = - stringResource( - id = - when (walletAddress) { - is WalletAddress.Unified -> R.string.receive_unified_content_description - is WalletAddress.Sapling -> R.string.receive_sapling_content_description - is WalletAddress.Transparent -> R.string.receive_transparent_content_description - } - ), - modifier = Modifier.align(Alignment.CenterHorizontally), - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny)) - - Text( - text = walletAddress.address, - style = ZcashTheme.extendedTypography.addressStyle, - color = ZcashTheme.colors.textDescription, - textAlign = TextAlign.Center, - modifier = - Modifier - .align(Alignment.CenterHorizontally) - .clickable { onAddressCopyToClipboard(walletAddress.address) } - .padding(horizontal = ZcashTheme.dimens.spacingLarge) - .fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - Reference( - text = stringResource(id = R.string.receive_copy), - onClick = { onAddressCopyToClipboard(walletAddress.address) }, - textAlign = TextAlign.Center, - imageVector = ImageVector.vectorResource(R.drawable.copy), - imageContentDescription = null, - modifier = - Modifier - .wrapContentSize() - .padding(all = ZcashTheme.dimens.spacingDefault), - ) - Reference( - text = stringResource(id = R.string.receive_share), - onClick = { onQrImageShare(qrCodeImage) }, - textAlign = TextAlign.Center, - imageVector = ImageVector.vectorResource(R.drawable.share), - imageContentDescription = null, - modifier = - Modifier - .wrapContentSize() - .padding(all = ZcashTheme.dimens.spacingDefault), + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = pagerState, + userScrollEnabled = false + ) { index -> + AddressPage( + walletAddresses = walletAddresses, + modifier = Modifier.fillMaxSize(), + walletAddress = state[index], + versionInfo = versionInfo, + onAddressCopyToClipboard = onAddressCopyToClipboard, + onQrImageShare = onQrImageShare, ) } } } +@Suppress("LongMethod", "LongParameterList") +@Composable +private fun AddressPage( + walletAddresses: WalletAddresses, + walletAddress: WalletAddress, + versionInfo: VersionInfo, + onAddressCopyToClipboard: (String) -> Unit, + onQrImageShare: (ImageBitmap) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .verticalScroll(rememberScrollState()) + .padding( + horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular, + vertical = ZcashTheme.dimens.spacingDefault, + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + QrCode(walletAddress, onAddressCopyToClipboard, onQrImageShare) + + if (versionInfo.isTestnet && walletAddress is WalletAddress.Unified) { + QrCode(walletAddresses.sapling, onAddressCopyToClipboard, onQrImageShare) + } + } +} + +@Composable +@Suppress("LongMethod") +private fun ColumnScope.QrCode( + walletAddress: WalletAddress, + onAddressCopyToClipboard: (String) -> Unit, + onQrImageShare: (ImageBitmap) -> Unit, +) { + val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt() + val qrCodeImage = + remember { + qrCodeForAddress( + address = walletAddress.address, + size = sizePixels + ) + } + + QrCode( + qrCodeImage = qrCodeImage, + onQrImageBitmapShare = onQrImageShare, + contentDescription = + stringResource( + when (walletAddress) { + is WalletAddress.Unified -> R.string.receive_unified_content_description + is WalletAddress.Sapling -> R.string.receive_sapling_content_description + is WalletAddress.Transparent -> R.string.receive_transparent_content_description + } + ), + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny)) + + Text( + text = walletAddress.address, + style = ZcashTheme.extendedTypography.addressStyle, + color = ZcashTheme.colors.textDescription, + textAlign = TextAlign.Center, + modifier = + Modifier + .align(Alignment.CenterHorizontally) + .clickable { onAddressCopyToClipboard(walletAddress.address) } + .padding(horizontal = ZcashTheme.dimens.spacingLarge) + .fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Reference( + text = stringResource(id = R.string.receive_copy), + onClick = { onAddressCopyToClipboard(walletAddress.address) }, + textAlign = TextAlign.Center, + imageVector = ImageVector.vectorResource(R.drawable.copy), + imageContentDescription = null, + modifier = + Modifier + .wrapContentSize() + .padding(all = ZcashTheme.dimens.spacingDefault), + ) + Reference( + text = stringResource(id = R.string.receive_share), + onClick = { onQrImageShare(qrCodeImage) }, + textAlign = TextAlign.Center, + imageVector = ImageVector.vectorResource(R.drawable.share), + imageContentDescription = null, + modifier = + Modifier + .wrapContentSize() + .padding(all = ZcashTheme.dimens.spacingDefault), + ) + } +} + private fun qrCodeForAddress( address: String, size: Int, @@ -339,3 +345,22 @@ private fun QrCode( .then(modifier) ) } + +@Preview +@Composable +private fun ComposablePreview() = + ZcashTheme(forceDarkMode = false) { + Receive( + screenBrightnessState = ScreenBrightnessState.NORMAL, + walletAddress = runBlocking { WalletAddressesFixture.new() }, + snackbarHostState = SnackbarHostState(), + onSettings = {}, + onAdjustBrightness = {}, + onAddrCopyToClipboard = {}, + onQrImageShare = {}, + versionInfo = VersionInfoFixture.new(), + topAppBarSubTitleState = TopAppBarSubTitleState.None, + ) + } + +private val DEFAULT_QR_CODE_SIZE = 320.dp