[#1219] Current balances UI
* [#1219] Current balances UI - This represents UI changes for balance, change, and transaction on the Balances screen - Reworked StyledBalance to be more reusable, too - Adopted the latest SDK changes related to change pending and pending transactions - Closes #1224 - Closes #1219 * Adopted latest SDK snapshot version
This commit is contained in:
parent
c5efcabf4c
commit
1058802b19
|
@ -188,7 +188,7 @@ ZCASH_BIP39_VERSION=1.0.7
|
|||
ZXING_VERSION=3.5.2
|
||||
|
||||
# WARNING: Ensure a non-snapshot version is used before releasing to production.
|
||||
ZCASH_SDK_VERSION=2.0.6
|
||||
ZCASH_SDK_VERSION=2.0.6-SNAPSHOT
|
||||
|
||||
# Toolchain is the Java version used to build the application, which is separate from the
|
||||
# Java version used to run the application.
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
||||
@Preview
|
||||
|
@ -15,7 +17,10 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
|||
private fun CircularScreenProgressIndicatorComposablePreview() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
GradientSurface {
|
||||
CircularScreenProgressIndicator()
|
||||
Column {
|
||||
CircularScreenProgressIndicator()
|
||||
CircularSmallProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +35,22 @@ fun CircularScreenProgressIndicator(modifier: Modifier = Modifier) {
|
|||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.width(ZcashTheme.dimens.circularScreenProgressWidth)
|
||||
color = ZcashTheme.colors.progressBarScreen,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(ZcashTheme.dimens.circularScreenProgressWidth)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CircularSmallProgressIndicator(modifier: Modifier = Modifier) {
|
||||
CircularProgressIndicator(
|
||||
color = ZcashTheme.colors.progressBarSmall,
|
||||
strokeWidth = 2.dp,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(ZcashTheme.dimens.circularSmallProgressWidth)
|
||||
.then(modifier)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -30,7 +30,10 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import java.util.Locale
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
@ -67,12 +70,11 @@ private fun StyledBalanceComposablePreview() {
|
|||
GradientSurface {
|
||||
Column {
|
||||
StyledBalance(
|
||||
mainPart = "1,234.567",
|
||||
secondPart = "89012",
|
||||
balanceString = "1,234.56789012",
|
||||
textStyles =
|
||||
Pair(
|
||||
ZcashTheme.extendedTypography.balanceStyles.first,
|
||||
ZcashTheme.extendedTypography.balanceStyles.second
|
||||
ZcashTheme.extendedTypography.balanceWidgetStyles.first,
|
||||
ZcashTheme.extendedTypography.balanceWidgetStyles.second
|
||||
),
|
||||
modifier = Modifier
|
||||
)
|
||||
|
@ -222,19 +224,6 @@ fun Tiny(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListItem(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = ZcashTheme.extendedTypography.listItem,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListHeader(
|
||||
text: String,
|
||||
|
@ -293,44 +282,78 @@ fun Reference(
|
|||
}
|
||||
|
||||
/**
|
||||
* Pass amount of Zcash tokens you want to display and the component style it according to the design requirements.
|
||||
* This accepts string with balance and displays it in the UI component styled according to the design
|
||||
* requirements. The function displays the balance within two parts.
|
||||
*
|
||||
* @param mainPart of Zcash tokens to be displayed in a bigger font style
|
||||
* @param secondPart of Zcash tokens to be displayed in a smaller font style
|
||||
* @param modifier to modify the Text UI element as needed
|
||||
* @param balanceString String of Zcash formatted balance
|
||||
* @param textStyles Styles for the first and second part of the balance
|
||||
* @param textColor Optional color to modify the default font color from [textStyles]
|
||||
* @param modifier Modifier to modify the Text UI element as needed
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun StyledBalance(
|
||||
mainPart: String,
|
||||
secondPart: String,
|
||||
balanceString: String,
|
||||
textStyles: Pair<TextStyle, TextStyle>,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
textColor: Color? = null
|
||||
) {
|
||||
val balanceSplit = splitBalance(balanceString)
|
||||
|
||||
val content =
|
||||
buildAnnotatedString {
|
||||
withStyle(
|
||||
style = textStyles.first.toSpanStyle()
|
||||
) {
|
||||
append(mainPart)
|
||||
append(balanceSplit.first)
|
||||
}
|
||||
withStyle(
|
||||
style = textStyles.second.toSpanStyle()
|
||||
) {
|
||||
append(secondPart)
|
||||
append(balanceSplit.second)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = content,
|
||||
// fixme color
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
modifier =
|
||||
Modifier
|
||||
.basicMarquee()
|
||||
.then(modifier)
|
||||
)
|
||||
if (textColor != null) {
|
||||
Text(
|
||||
text = content,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
modifier =
|
||||
Modifier
|
||||
.basicMarquee()
|
||||
.then(modifier)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = content,
|
||||
maxLines = 1,
|
||||
modifier =
|
||||
Modifier
|
||||
.basicMarquee()
|
||||
.then(modifier)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun splitBalance(balance: String): Pair<String, String> {
|
||||
Twig.debug { "Balance before split: $balance" }
|
||||
|
||||
@Suppress("MAGIC_CONSTANT", "MagicNumber")
|
||||
val cutPosition = balance.indexOf(MonetarySeparators.current(Locale.US).decimal) + 4
|
||||
val firstPart =
|
||||
balance.substring(
|
||||
startIndex = 0,
|
||||
endIndex = cutPosition
|
||||
)
|
||||
val secondPart =
|
||||
balance.substring(
|
||||
startIndex = cutPosition
|
||||
)
|
||||
|
||||
Twig.debug { "Balance after split: $firstPart|$secondPart" }
|
||||
|
||||
return Pair(firstPart, secondPart)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -30,6 +30,7 @@ data class Dimens(
|
|||
val chipStroke: Dp,
|
||||
// Progress
|
||||
val circularScreenProgressWidth: Dp,
|
||||
val circularSmallProgressWidth: Dp,
|
||||
// TopAppBar:
|
||||
val topAppBarZcashLogoHeight: Dp,
|
||||
val topAppBarActionRippleCorner: Dp,
|
||||
|
@ -37,6 +38,7 @@ data class Dimens(
|
|||
val textFieldDefaultHeight: Dp,
|
||||
val textFieldPanelDefaultHeight: Dp,
|
||||
// Any Layout:
|
||||
val divider: Dp,
|
||||
val layoutStroke: Dp,
|
||||
// Screen custom spacings:
|
||||
val inScreenZcashLogoHeight: Dp,
|
||||
|
@ -64,11 +66,13 @@ private val defaultDimens =
|
|||
chipShadowElevation = 4.dp,
|
||||
chipStroke = 0.5.dp,
|
||||
circularScreenProgressWidth = 48.dp,
|
||||
circularSmallProgressWidth = 14.dp,
|
||||
topAppBarZcashLogoHeight = 24.dp,
|
||||
topAppBarActionRippleCorner = 28.dp,
|
||||
textFieldDefaultHeight = 64.dp,
|
||||
textFieldPanelDefaultHeight = 215.dp,
|
||||
layoutStroke = 1.dp,
|
||||
divider = 1.dp,
|
||||
inScreenZcashLogoHeight = 100.dp,
|
||||
inScreenZcashLogoWidth = 60.dp,
|
||||
inScreenZcashTextLogoHeight = 30.dp,
|
||||
|
|
|
@ -14,12 +14,13 @@ data class ExtendedColors(
|
|||
val onTertiary: Color,
|
||||
val callout: Color,
|
||||
val onCallout: Color,
|
||||
val progressStart: Color,
|
||||
val progressEnd: Color,
|
||||
val progressBackground: Color,
|
||||
val progressBarSmall: Color,
|
||||
val progressBarScreen: Color,
|
||||
val chipIndex: Color,
|
||||
val textCommon: Color,
|
||||
val textFieldHint: Color,
|
||||
val textDescription: Color,
|
||||
val textPending: Color,
|
||||
val layoutStroke: Color,
|
||||
val overlay: Color,
|
||||
val highlight: Color,
|
||||
|
@ -34,6 +35,7 @@ data class ExtendedColors(
|
|||
val welcomeAnimationColor: Color,
|
||||
val complementaryColor: Color,
|
||||
val dividerColor: Color,
|
||||
val darkDividerColor: Color,
|
||||
val tabTextColor: Color,
|
||||
) {
|
||||
@Composable
|
||||
|
|
|
@ -24,10 +24,11 @@ internal object Dark {
|
|||
val textSecondaryButton = Color(0xFF000000)
|
||||
val textTertiaryButton = Color.White
|
||||
val textNavigationButton = Color.Black
|
||||
val textCaption = Color(0xFFFFFFFF)
|
||||
val textCommon = Color(0xFFFFFFFF)
|
||||
val textChipIndex = Color(0xFFFFB900)
|
||||
val textFieldHint = Color(0xFFB7B7B7)
|
||||
val textDescription = Color(0xFF777777)
|
||||
val textProgress = Color(0xFF8B8A8A)
|
||||
|
||||
val layoutStroke = Color(0xFFFFFFFF)
|
||||
|
||||
|
@ -45,9 +46,8 @@ internal object Dark {
|
|||
val navigationButton = Color(0xFFFFFFFF)
|
||||
val navigationButtonPressed = Color(0xFFFFFFFF)
|
||||
|
||||
val progressStart = Color(0xFFF364CE)
|
||||
val progressEnd = Color(0xFFF8964F)
|
||||
val progressBackground = Color(0xFF929bb3)
|
||||
val progressBarSmall = Color(0xFF8B8A8A)
|
||||
val progressBarScreen = Color(0xFFFFFFFF)
|
||||
|
||||
val callout = Color(0xFFFFFFFF)
|
||||
val onCallout = Color(0xFFFFFFFF)
|
||||
|
@ -55,11 +55,6 @@ internal object Dark {
|
|||
val overlay = Color(0x22000000)
|
||||
val highlight = Color(0xFFFFD800)
|
||||
|
||||
val addressHighlightBorder = Color(0xFF525252)
|
||||
val addressHighlightUnified = Color(0xFFFFD800)
|
||||
val addressHighlightSapling = Color(0xFF1BBFF6)
|
||||
val addressHighlightTransparent = Color(0xFF97999A)
|
||||
|
||||
val dangerous = Color(0xFFEC0008)
|
||||
val onDangerous = Color(0xFFFFFFFF)
|
||||
|
||||
|
@ -76,6 +71,7 @@ internal object Dark {
|
|||
val welcomeAnimationColor = Color(0xFF231F20)
|
||||
val complementaryColor = Color(0xFFF4B728)
|
||||
val dividerColor = Color(0xFFDDDDDD)
|
||||
val darkDividerColor = Color(0xFF000000)
|
||||
val tabTextColor = Color(0xFF040404)
|
||||
}
|
||||
|
||||
|
@ -89,10 +85,11 @@ internal object Light {
|
|||
val textPrimaryButton = Color(0xFFFFFFFF)
|
||||
val textSecondaryButton = Color(0xFF000000)
|
||||
val textTertiaryButton = Color(0xFF000000)
|
||||
val textCaption = Color(0xFF000000)
|
||||
val textCommon = Color(0xFF000000)
|
||||
val textChipIndex = Color(0xFFEE8592)
|
||||
val textFieldHint = Color(0xFFB7B7B7)
|
||||
val textDescription = Color(0xFF777777)
|
||||
val textProgress = Color(0xFF8B8A8A)
|
||||
|
||||
val layoutStroke = Color(0xFF000000)
|
||||
|
||||
|
@ -110,9 +107,8 @@ internal object Light {
|
|||
val navigationButton = Color(0xFFFFFFFF)
|
||||
val navigationButtonPressed = Color(0xFFFFFFFF)
|
||||
|
||||
val progressStart = Color(0xFFF364CE)
|
||||
val progressEnd = Color(0xFFF8964F)
|
||||
val progressBackground = Color(0xFFFFFFFF)
|
||||
val progressBarSmall = Color(0xFF8B8A8A)
|
||||
val progressBarScreen = Color(0xFF000000)
|
||||
|
||||
val callout = Color(0xFFFFFFFF)
|
||||
val onCallout = Color(0xFFFFFFFF)
|
||||
|
@ -134,6 +130,7 @@ internal object Light {
|
|||
val welcomeAnimationColor = Color(0xFF231F20)
|
||||
val complementaryColor = Color(0xFFF4B728)
|
||||
val dividerColor = Color(0xFFDDDDDD)
|
||||
val darkDividerColor = Color(0xFF000000)
|
||||
val tabTextColor = Color(0xFF040404)
|
||||
}
|
||||
|
||||
|
@ -169,12 +166,13 @@ internal val DarkExtendedColorPalette =
|
|||
onTertiary = Dark.textTertiaryButton,
|
||||
callout = Dark.callout,
|
||||
onCallout = Dark.onCallout,
|
||||
progressStart = Dark.progressStart,
|
||||
progressEnd = Dark.progressEnd,
|
||||
progressBackground = Dark.progressBackground,
|
||||
progressBarSmall = Dark.progressBarSmall,
|
||||
progressBarScreen = Dark.progressBarScreen,
|
||||
chipIndex = Dark.textChipIndex,
|
||||
textCommon = Dark.textCommon,
|
||||
textFieldHint = Dark.textFieldHint,
|
||||
textDescription = Dark.textDescription,
|
||||
textPending = Dark.textProgress,
|
||||
layoutStroke = Dark.layoutStroke,
|
||||
overlay = Dark.overlay,
|
||||
highlight = Dark.highlight,
|
||||
|
@ -189,6 +187,7 @@ internal val DarkExtendedColorPalette =
|
|||
welcomeAnimationColor = Dark.welcomeAnimationColor,
|
||||
complementaryColor = Dark.complementaryColor,
|
||||
dividerColor = Dark.dividerColor,
|
||||
darkDividerColor = Dark.darkDividerColor,
|
||||
tabTextColor = Dark.tabTextColor,
|
||||
)
|
||||
|
||||
|
@ -200,12 +199,13 @@ internal val LightExtendedColorPalette =
|
|||
onTertiary = Light.textTertiaryButton,
|
||||
callout = Light.callout,
|
||||
onCallout = Light.onCallout,
|
||||
progressStart = Light.progressStart,
|
||||
progressEnd = Light.progressEnd,
|
||||
progressBackground = Light.progressBackground,
|
||||
progressBarScreen = Light.progressBarScreen,
|
||||
progressBarSmall = Light.progressBarSmall,
|
||||
chipIndex = Light.textChipIndex,
|
||||
textCommon = Light.textCommon,
|
||||
textFieldHint = Light.textFieldHint,
|
||||
textDescription = Light.textDescription,
|
||||
textPending = Light.textProgress,
|
||||
layoutStroke = Light.layoutStroke,
|
||||
overlay = Light.overlay,
|
||||
highlight = Light.highlight,
|
||||
|
@ -220,7 +220,8 @@ internal val LightExtendedColorPalette =
|
|||
welcomeAnimationColor = Light.welcomeAnimationColor,
|
||||
complementaryColor = Light.complementaryColor,
|
||||
dividerColor = Light.dividerColor,
|
||||
tabTextColor = Dark.tabTextColor,
|
||||
darkDividerColor = Light.darkDividerColor,
|
||||
tabTextColor = Light.tabTextColor,
|
||||
)
|
||||
|
||||
@Suppress("CompositionLocalAllowlist")
|
||||
|
@ -233,12 +234,13 @@ internal val LocalExtendedColors =
|
|||
onTertiary = Color.Unspecified,
|
||||
callout = Color.Unspecified,
|
||||
onCallout = Color.Unspecified,
|
||||
progressStart = Color.Unspecified,
|
||||
progressEnd = Color.Unspecified,
|
||||
progressBackground = Color.Unspecified,
|
||||
progressBarScreen = Color.Unspecified,
|
||||
progressBarSmall = Color.Unspecified,
|
||||
chipIndex = Color.Unspecified,
|
||||
textCommon = Color.Unspecified,
|
||||
textFieldHint = Color.Unspecified,
|
||||
textDescription = Color.Unspecified,
|
||||
textPending = Color.Unspecified,
|
||||
layoutStroke = Color.Unspecified,
|
||||
overlay = Color.Unspecified,
|
||||
highlight = Color.Unspecified,
|
||||
|
@ -253,6 +255,7 @@ internal val LocalExtendedColors =
|
|||
welcomeAnimationColor = Color.Unspecified,
|
||||
complementaryColor = Color.Unspecified,
|
||||
dividerColor = Color.Unspecified,
|
||||
darkDividerColor = Color.Unspecified,
|
||||
tabTextColor = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
|
|
@ -73,8 +73,8 @@ internal val PrimaryTypography =
|
|||
bodySmall =
|
||||
TextStyle(
|
||||
fontFamily = InterFontFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
labelLarge =
|
||||
TextStyle(
|
||||
|
@ -139,18 +139,26 @@ data class Typography(
|
|||
)
|
||||
|
||||
@Immutable
|
||||
data class BalanceTextStyles(
|
||||
data class BalanceWidgetTextStyles(
|
||||
val first: TextStyle,
|
||||
val second: TextStyle,
|
||||
val third: TextStyle,
|
||||
val fourth: TextStyle,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class BalanceSingleTextStyles(
|
||||
val first: TextStyle,
|
||||
val second: TextStyle,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ExtendedTypography(
|
||||
val listItem: TextStyle,
|
||||
// Grouping balances text styles to a wrapper class
|
||||
val balanceStyles: BalanceTextStyles,
|
||||
// Grouping balances text styles to a wrapper class for BalanceWidget
|
||||
val balanceWidgetStyles: BalanceWidgetTextStyles,
|
||||
// Grouping balances text styles to a wrapper class for single balance use case
|
||||
val balanceSingleStyles: BalanceSingleTextStyles,
|
||||
val addressStyle: TextStyle,
|
||||
val aboutText: TextStyle,
|
||||
val buttonText: TextStyle,
|
||||
|
@ -180,8 +188,8 @@ val LocalExtendedTypography =
|
|||
fontSize = 24.sp
|
||||
),
|
||||
// Note: the order here matters, be careful when reordering
|
||||
balanceStyles =
|
||||
BalanceTextStyles(
|
||||
balanceWidgetStyles =
|
||||
BalanceWidgetTextStyles(
|
||||
first =
|
||||
SecondaryTypography.headlineLarge.copy(
|
||||
fontSize = 42.sp,
|
||||
|
@ -205,6 +213,19 @@ val LocalExtendedTypography =
|
|||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
),
|
||||
balanceSingleStyles =
|
||||
BalanceSingleTextStyles(
|
||||
first =
|
||||
SecondaryTypography.bodySmall.copy(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
),
|
||||
second =
|
||||
SecondaryTypography.bodySmall.copy(
|
||||
fontSize = 8.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
),
|
||||
addressStyle =
|
||||
SecondaryTypography.bodyLarge.copy(
|
||||
// TODO [#1032]: Addresses can be shown with "×" symbols
|
||||
|
|
|
@ -71,7 +71,7 @@ internal class MockSynchronizer : CloseableSynchronizer {
|
|||
override val transactions: Flow<List<TransactionOverview>>
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override val transparentBalances: StateFlow<WalletBalance?>
|
||||
override val transparentBalance: StateFlow<Zatoshi?>
|
||||
get() = error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
|
||||
override fun close() {
|
||||
|
@ -98,7 +98,7 @@ internal class MockSynchronizer : CloseableSynchronizer {
|
|||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
override suspend fun getTransparentBalance(tAddr: String): WalletBalance {
|
||||
override suspend fun getTransparentBalance(tAddr: String): Zatoshi {
|
||||
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
@ -101,8 +101,7 @@ class SendViewTestSetup(
|
|||
walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
saplingBalance =
|
||||
WalletBalance(
|
||||
total = Zatoshi(Zatoshi.MAX_INCLUSIVE.div(100)),
|
||||
WalletBalanceFixture.new(
|
||||
available = Zatoshi(Zatoshi.MAX_INCLUSIVE.div(100))
|
||||
)
|
||||
),
|
||||
|
|
|
@ -6,8 +6,8 @@ import androidx.compose.ui.test.junit4.StateRestorationTester
|
|||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
|
@ -44,8 +44,7 @@ class SendViewIntegrationTest {
|
|||
private val walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
saplingBalance =
|
||||
WalletBalance(
|
||||
total = Zatoshi(Zatoshi.MAX_INCLUSIVE.div(100)),
|
||||
WalletBalanceFixture.new(
|
||||
available = Zatoshi(Zatoshi.MAX_INCLUSIVE.div(100))
|
||||
)
|
||||
)
|
||||
|
|
|
@ -17,12 +17,10 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.totalBalance
|
||||
|
@ -33,7 +31,6 @@ import co.electriccoin.zcash.ui.design.component.Reference
|
|||
import co.electriccoin.zcash.ui.design.component.StyledBalance
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import java.util.Locale
|
||||
|
||||
@Preview(device = Devices.PIXEL_2)
|
||||
@Composable
|
||||
|
@ -49,7 +46,8 @@ private fun BalanceWidgetPreview() {
|
|||
saplingBalance =
|
||||
WalletBalance(
|
||||
Zatoshi(1234567891234567),
|
||||
Zatoshi(123456789)
|
||||
Zatoshi(123456789),
|
||||
Zatoshi(123)
|
||||
)
|
||||
),
|
||||
isReferenceToBalances = true,
|
||||
|
@ -60,26 +58,6 @@ private fun BalanceWidgetPreview() {
|
|||
}
|
||||
}
|
||||
|
||||
fun splitBalance(balance: String): Pair<String, String> {
|
||||
Twig.debug { "Balance before split: $balance" }
|
||||
|
||||
@Suppress("MAGIC_CONSTANT", "MagicNumber")
|
||||
val cutPosition = balance.indexOf(MonetarySeparators.current(Locale.US).decimal) + 4
|
||||
val firstPart =
|
||||
balance.substring(
|
||||
startIndex = 0,
|
||||
endIndex = cutPosition
|
||||
)
|
||||
val secondPart =
|
||||
balance.substring(
|
||||
startIndex = cutPosition
|
||||
)
|
||||
|
||||
Twig.debug { "Balance after split: $firstPart|$secondPart" }
|
||||
|
||||
return Pair(firstPart, secondPart)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun BalanceWidget(
|
||||
|
@ -95,18 +73,15 @@ fun BalanceWidget(
|
|||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val totalBalanceSplit = splitBalance(walletSnapshot.totalBalance().toZecString())
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StyledBalance(
|
||||
mainPart = totalBalanceSplit.first,
|
||||
secondPart = totalBalanceSplit.second,
|
||||
balanceString = walletSnapshot.totalBalance().toZecString(),
|
||||
textStyles =
|
||||
Pair(
|
||||
ZcashTheme.extendedTypography.balanceStyles.first,
|
||||
ZcashTheme.extendedTypography.balanceStyles.second
|
||||
ZcashTheme.extendedTypography.balanceWidgetStyles.first,
|
||||
ZcashTheme.extendedTypography.balanceWidgetStyles.second
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -136,15 +111,12 @@ fun BalanceWidget(
|
|||
|
||||
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny))
|
||||
|
||||
val availableBalanceSplit = splitBalance(walletSnapshot.spendableBalance().toZecString())
|
||||
|
||||
StyledBalance(
|
||||
mainPart = availableBalanceSplit.first,
|
||||
secondPart = availableBalanceSplit.second,
|
||||
balanceString = walletSnapshot.spendableBalance().toZecString(),
|
||||
textStyles =
|
||||
Pair(
|
||||
ZcashTheme.extendedTypography.balanceStyles.third,
|
||||
ZcashTheme.extendedTypography.balanceStyles.fourth
|
||||
ZcashTheme.extendedTypography.balanceWidgetStyles.third,
|
||||
ZcashTheme.extendedTypography.balanceWidgetStyles.fourth
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ data class WalletSnapshot(
|
|||
val processorInfo: CompactBlockProcessor.ProcessorInfo,
|
||||
val orchardBalance: WalletBalance,
|
||||
val saplingBalance: WalletBalance,
|
||||
val transparentBalance: WalletBalance,
|
||||
val transparentBalance: Zatoshi,
|
||||
val progress: PercentDecimal,
|
||||
val synchronizerError: SynchronizerError?
|
||||
) {
|
||||
|
@ -28,9 +28,15 @@ data class WalletSnapshot(
|
|||
val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasFunds
|
||||
}
|
||||
|
||||
fun WalletSnapshot.totalBalance() = orchardBalance.total + saplingBalance.total + transparentBalance.total
|
||||
fun WalletSnapshot.totalBalance() = orchardBalance.total + saplingBalance.total + transparentBalance
|
||||
|
||||
// Note that considering both to be spendable is subject to change.
|
||||
// The user experience could be confusing, and in the future we might prefer to ask users
|
||||
// to transfer their balance to the latest balance type to make it spendable.
|
||||
fun WalletSnapshot.spendableBalance() = orchardBalance.available + saplingBalance.available
|
||||
|
||||
// Note that summing both values could be confusing, and we might prefer dividing them in the future
|
||||
fun WalletSnapshot.changePendingBalance() = orchardBalance.changePending + saplingBalance.changePending
|
||||
|
||||
// Note that summing both values could be confusing, and we might prefer dividing them in the future
|
||||
fun WalletSnapshot.valuePendingBalance() = orchardBalance.valuePending + saplingBalance.valuePending
|
||||
|
|
|
@ -378,7 +378,7 @@ private fun Synchronizer.toWalletSnapshot() =
|
|||
// 3
|
||||
saplingBalances,
|
||||
// 4
|
||||
transparentBalances,
|
||||
transparentBalance,
|
||||
// 5
|
||||
progress,
|
||||
// 6
|
||||
|
@ -386,16 +386,16 @@ private fun Synchronizer.toWalletSnapshot() =
|
|||
) { flows ->
|
||||
val orchardBalance = flows[2] as WalletBalance?
|
||||
val saplingBalance = flows[3] as WalletBalance?
|
||||
val transparentBalance = flows[4] as WalletBalance?
|
||||
val transparentBalance = flows[4] as Zatoshi?
|
||||
|
||||
val progressPercentDecimal = flows[5] as PercentDecimal
|
||||
|
||||
WalletSnapshot(
|
||||
flows[0] as Synchronizer.Status,
|
||||
flows[1] as CompactBlockProcessor.ProcessorInfo,
|
||||
orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
||||
saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
||||
transparentBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
||||
orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
|
||||
saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
|
||||
transparentBalance ?: Zatoshi(0),
|
||||
progressPercentDecimal,
|
||||
flows[6] as SynchronizerError?
|
||||
)
|
||||
|
|
|
@ -2,9 +2,11 @@ package co.electriccoin.zcash.ui.fixture
|
|||
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
|
||||
|
||||
|
@ -12,9 +14,9 @@ import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
|
|||
object WalletSnapshotFixture {
|
||||
val STATUS = Synchronizer.Status.SYNCED
|
||||
val PROGRESS = PercentDecimal.ZERO_PERCENT
|
||||
val TRANSPARENT_BALANCE: WalletBalance = WalletBalance(Zatoshi(8), Zatoshi(1))
|
||||
val ORCHARD_BALANCE: WalletBalance = WalletBalance(Zatoshi(5), Zatoshi(2))
|
||||
val SAPLING_BALANCE: WalletBalance = WalletBalance(Zatoshi(4), Zatoshi(4))
|
||||
val TRANSPARENT_BALANCE: Zatoshi = ZatoshiFixture.new(8)
|
||||
val ORCHARD_BALANCE: WalletBalance = WalletBalanceFixture.newLong(8, 2, 1)
|
||||
val SAPLING_BALANCE: WalletBalance = WalletBalanceFixture.newLong(5, 2, 1)
|
||||
|
||||
// Should fill in with non-empty values for better example values in tests and UI previews
|
||||
@Suppress("LongParameterList")
|
||||
|
@ -28,7 +30,7 @@ object WalletSnapshotFixture {
|
|||
),
|
||||
orchardBalance: WalletBalance = ORCHARD_BALANCE,
|
||||
saplingBalance: WalletBalance = SAPLING_BALANCE,
|
||||
transparentBalance: WalletBalance = TRANSPARENT_BALANCE,
|
||||
transparentBalance: Zatoshi = TRANSPARENT_BALANCE,
|
||||
progress: PercentDecimal = PROGRESS,
|
||||
synchronizerError: SynchronizerError? = null
|
||||
) = WalletSnapshot(
|
||||
|
|
|
@ -1,32 +1,49 @@
|
|||
package co.electriccoin.zcash.ui.screen.balances.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.android.sdk.model.toZecString
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.changePendingBalance
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.valuePendingBalance
|
||||
import co.electriccoin.zcash.ui.common.test.CommonTag
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.BodySmall
|
||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.CircularSmallProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.StyledBalance
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.account.model.WalletDisplayValues
|
||||
|
@ -106,7 +123,7 @@ private fun BalancesMainContent(
|
|||
modifier =
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
// .verticalScroll(rememberScrollState()) Uncomment this once the whole screen UI is implemented
|
||||
.verticalScroll(rememberScrollState())
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
|
@ -118,20 +135,157 @@ private fun BalancesMainContent(
|
|||
isReferenceToBalances = false,
|
||||
onReferenceClick = {}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||
|
||||
Divider(
|
||||
color = ZcashTheme.colors.darkDividerColor,
|
||||
thickness = ZcashTheme.dimens.divider
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
BalancesOverview(walletSnapshot)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
|
||||
|
||||
Body(
|
||||
text = stringResource(id = R.string.balances_coming_soon),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BalancesOverview(walletSnapshot: WalletSnapshot) {
|
||||
Column {
|
||||
SpendableBalanceRow(walletSnapshot)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
ChangePendingRow(walletSnapshot)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
// aka value pending
|
||||
PendingTransactionsRow(walletSnapshot)
|
||||
}
|
||||
}
|
||||
|
||||
const val TEXT_PART_WIDTH_RATIO = 0.6f
|
||||
|
||||
@Composable
|
||||
fun SpendableBalanceRow(walletSnapshot: WalletSnapshot) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
BodySmall(
|
||||
text = stringResource(id = R.string.balances_shielded_spendable).uppercase(),
|
||||
modifier = Modifier.fillMaxWidth(TEXT_PART_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
StyledBalance(
|
||||
balanceString = walletSnapshot.spendableBalance().toZecString(),
|
||||
textStyles =
|
||||
Pair(
|
||||
ZcashTheme.extendedTypography.balanceSingleStyles.first,
|
||||
ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
textColor = ZcashTheme.colors.textCommon
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.balance_shield),
|
||||
contentDescription = null,
|
||||
// The same size as the following progress bars
|
||||
modifier = Modifier.width(ZcashTheme.dimens.circularSmallProgressWidth)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangePendingRow(walletSnapshot: WalletSnapshot) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
BodySmall(
|
||||
text = stringResource(id = R.string.balances_change_pending).uppercase(),
|
||||
modifier = Modifier.fillMaxWidth(TEXT_PART_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val changePendingHasValue = walletSnapshot.changePendingBalance().value > 0L
|
||||
|
||||
StyledBalance(
|
||||
balanceString = walletSnapshot.changePendingBalance().toZecString(),
|
||||
textStyles =
|
||||
Pair(
|
||||
ZcashTheme.extendedTypography.balanceSingleStyles.first,
|
||||
ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
textColor = ZcashTheme.colors.textPending
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Box(Modifier.width(ZcashTheme.dimens.circularSmallProgressWidth)) {
|
||||
if (changePendingHasValue) {
|
||||
CircularSmallProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PendingTransactionsRow(walletSnapshot: WalletSnapshot) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
BodySmall(
|
||||
text = stringResource(id = R.string.balances_pending_transactions).uppercase(),
|
||||
modifier = Modifier.fillMaxWidth(TEXT_PART_WIDTH_RATIO)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val valuePendingHasValue = walletSnapshot.valuePendingBalance().value > 0L
|
||||
|
||||
StyledBalance(
|
||||
balanceString = walletSnapshot.valuePendingBalance().toZecString(),
|
||||
textStyles =
|
||||
Pair(
|
||||
ZcashTheme.extendedTypography.balanceSingleStyles.first,
|
||||
ZcashTheme.extendedTypography.balanceSingleStyles.second
|
||||
),
|
||||
textColor = ZcashTheme.colors.textPending
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Box(Modifier.width(ZcashTheme.dimens.circularSmallProgressWidth)) {
|
||||
if (valuePendingHasValue) {
|
||||
CircularSmallProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="11dp"
|
||||
android:height="14dp"
|
||||
android:viewportWidth="11"
|
||||
android:viewportHeight="14">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h11v14h-11z"/>
|
||||
<path
|
||||
android:pathData="M10.727,1.675L5.61,0.032C5.537,0.004 5.463,0.004 5.39,0.032L0.272,1.675C0.04,1.748 0,1.804 0,2.067V8.092C0,8.875 0.297,9.677 0.881,10.476C1.328,11.086 1.946,11.698 2.717,12.295C4.014,13.3 5.291,13.916 5.344,13.941C5.448,13.993 5.552,13.993 5.657,13.941C5.71,13.916 6.986,13.3 8.284,12.295C9.055,11.698 9.673,11.085 10.12,10.476C10.704,9.677 11.001,8.875 11.001,8.092V2.067C10.988,1.794 10.915,1.756 10.729,1.675H10.727Z"
|
||||
android:fillColor="#231F20"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -1,5 +1,8 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="balances_title">Balances</string>
|
||||
<string name="balances_coming_soon">Coming soon:\n\nBalances overview,\nTransparent
|
||||
<string name="balances_shielded_spendable">Shielded zec (spendable)</string>
|
||||
<string name="balances_change_pending">Change pending</string>
|
||||
<string name="balances_pending_transactions">Pending transactions</string>
|
||||
<string name="balances_coming_soon">Coming soon:\n\nTransparent
|
||||
funds shielding,\nBlock synchronization indicator</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue