[#1284] Rework Scan screen
- Closes #1284 - Closes #423 - Closes #437 - Changelog update
This commit is contained in:
parent
992c1dd197
commit
1ffbaf986f
|
@ -9,6 +9,9 @@ directly impact users rather than highlighting other key architectural updates.*
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- The Scan QR code screen has been reworked to align with the rest of the screens
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Sending zero funds is allowed only for shielded recipient address type
|
- Sending zero funds is allowed only for shielded recipient address type
|
||||||
- The Balances widget loader has been improved to better handle cases, like a wallet with only transparent funds
|
- The Balances widget loader has been improved to better handle cases, like a wallet with only transparent funds
|
||||||
|
|
|
@ -183,7 +183,7 @@ fun SecondaryButton(
|
||||||
horizontal = ZcashTheme.dimens.spacingNone,
|
horizontal = ZcashTheme.dimens.spacingNone,
|
||||||
vertical = ZcashTheme.dimens.spacingSmall
|
vertical = ZcashTheme.dimens.spacingSmall
|
||||||
),
|
),
|
||||||
contentPaddingValues: PaddingValues = PaddingValues(all = 16.dp)
|
contentPaddingValues: PaddingValues = PaddingValues(all = 16.5.dp)
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
|
@ -195,7 +195,8 @@ fun SecondaryButton(
|
||||||
.padding(outerPaddingValues)
|
.padding(outerPaddingValues)
|
||||||
.shadow(
|
.shadow(
|
||||||
contentColor = textColor,
|
contentColor = textColor,
|
||||||
strokeColor = textColor,
|
strokeColor = buttonColor,
|
||||||
|
strokeWidth = 1.dp,
|
||||||
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
|
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
|
||||||
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
|
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
|
||||||
spread = ZcashTheme.dimens.buttonShadowSpread,
|
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
package co.electriccoin.zcash.ui.design.component
|
package co.electriccoin.zcash.ui.design.component
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -44,6 +43,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import co.electriccoin.zcash.ui.design.R
|
import co.electriccoin.zcash.ui.design.R
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.design.theme.internal.SecondaryTypography
|
import co.electriccoin.zcash.ui.design.theme.internal.SecondaryTypography
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.internal.TopAppBarColors
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -265,14 +265,15 @@ private fun TopBarOneVisibleActionMenuExample(
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
fun SmallTopAppBar(
|
fun SmallTopAppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
restoringLabel: String? = null,
|
|
||||||
titleText: String? = null,
|
|
||||||
showTitleLogo: Boolean = false,
|
|
||||||
backText: String? = null,
|
|
||||||
backContentDescriptionText: String? = null,
|
backContentDescriptionText: String? = null,
|
||||||
onBack: (() -> Unit)? = null,
|
backText: String? = null,
|
||||||
|
colors: TopAppBarColors = ZcashTheme.colors.topAppBarColors,
|
||||||
hamburgerMenuActions: (@Composable RowScope.() -> Unit)? = null,
|
hamburgerMenuActions: (@Composable RowScope.() -> Unit)? = null,
|
||||||
|
onBack: (() -> Unit)? = null,
|
||||||
regularActions: (@Composable RowScope.() -> Unit)? = null,
|
regularActions: (@Composable RowScope.() -> Unit)? = null,
|
||||||
|
restoringLabel: String? = null,
|
||||||
|
showTitleLogo: Boolean = false,
|
||||||
|
titleText: String? = null,
|
||||||
) {
|
) {
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
|
@ -284,13 +285,15 @@ fun SmallTopAppBar(
|
||||||
if (titleText != null) {
|
if (titleText != null) {
|
||||||
Text(
|
Text(
|
||||||
text = titleText.uppercase(),
|
text = titleText.uppercase(),
|
||||||
style = SecondaryTypography.headlineSmall
|
style = SecondaryTypography.headlineSmall,
|
||||||
|
color = colors.titleColor,
|
||||||
)
|
)
|
||||||
restoringSpacerHeight = ZcashTheme.dimens.spacingTiny
|
restoringSpacerHeight = ZcashTheme.dimens.spacingTiny
|
||||||
} else if (showTitleLogo) {
|
} else if (showTitleLogo) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.zashi_text_logo),
|
painter = painterResource(id = R.drawable.zashi_text_logo),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
tint = colors.titleColor,
|
||||||
modifier = Modifier.height(ZcashTheme.dimens.topAppBarZcashLogoHeight)
|
modifier = Modifier.height(ZcashTheme.dimens.topAppBarZcashLogoHeight)
|
||||||
)
|
)
|
||||||
restoringSpacerHeight = ZcashTheme.dimens.spacingSmall
|
restoringSpacerHeight = ZcashTheme.dimens.spacingSmall
|
||||||
|
@ -303,7 +306,7 @@ fun SmallTopAppBar(
|
||||||
Text(
|
Text(
|
||||||
text = restoringLabel.uppercase(),
|
text = restoringLabel.uppercase(),
|
||||||
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
|
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
|
||||||
color = ZcashTheme.colors.restoringTopAppBarColor,
|
color = colors.subTitleColor,
|
||||||
modifier = Modifier.fillMaxWidth(0.75f),
|
modifier = Modifier.fillMaxWidth(0.75f),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
@ -325,9 +328,10 @@ fun SmallTopAppBar(
|
||||||
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingDefault),
|
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingDefault),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = backContentDescriptionText
|
contentDescription = backContentDescriptionText,
|
||||||
|
tint = colors.navigationColor,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(size = ZcashTheme.dimens.spacingSmall))
|
Spacer(modifier = Modifier.size(size = ZcashTheme.dimens.spacingSmall))
|
||||||
Text(text = backText.uppercase())
|
Text(text = backText.uppercase())
|
||||||
|
@ -339,6 +343,7 @@ fun SmallTopAppBar(
|
||||||
regularActions?.invoke(this)
|
regularActions?.invoke(this)
|
||||||
hamburgerMenuActions?.invoke(this)
|
hamburgerMenuActions?.invoke(this)
|
||||||
},
|
},
|
||||||
|
colors = colors.toMaterialTopAppBarColors(),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.testTag(CommonTag.TOP_APP_BAR)
|
.testTag(CommonTag.TOP_APP_BAR)
|
||||||
|
|
|
@ -55,6 +55,7 @@ data class Dimens(
|
||||||
val inScreenZcashTextLogoHeight: Dp,
|
val inScreenZcashTextLogoHeight: Dp,
|
||||||
val screenHorizontalSpacingBig: Dp,
|
val screenHorizontalSpacingBig: Dp,
|
||||||
val screenHorizontalSpacingRegular: Dp,
|
val screenHorizontalSpacingRegular: Dp,
|
||||||
|
val cameraTorchButton: Dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val defaultDimens =
|
private val defaultDimens =
|
||||||
|
@ -96,6 +97,7 @@ private val defaultDimens =
|
||||||
inScreenZcashTextLogoHeight = 30.dp,
|
inScreenZcashTextLogoHeight = 30.dp,
|
||||||
screenHorizontalSpacingBig = 64.dp,
|
screenHorizontalSpacingBig = 64.dp,
|
||||||
screenHorizontalSpacingRegular = 32.dp,
|
screenHorizontalSpacingRegular = 32.dp,
|
||||||
|
cameraTorchButton = 20.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val normalDimens = defaultDimens
|
private val normalDimens = defaultDimens
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import co.electriccoin.zcash.ui.design.theme.internal.TopAppBarColors
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ExtendedColors(
|
data class ExtendedColors(
|
||||||
|
@ -19,7 +20,6 @@ data class ExtendedColors(
|
||||||
val circularProgressBarScreen: Color,
|
val circularProgressBarScreen: Color,
|
||||||
val linearProgressBarTrack: Color,
|
val linearProgressBarTrack: Color,
|
||||||
val linearProgressBarBackground: Color,
|
val linearProgressBarBackground: Color,
|
||||||
val restoringTopAppBarColor: Color,
|
|
||||||
val chipIndex: Color,
|
val chipIndex: Color,
|
||||||
val textCommon: Color,
|
val textCommon: Color,
|
||||||
val textMedium: Color,
|
val textMedium: Color,
|
||||||
|
@ -45,11 +45,15 @@ data class ExtendedColors(
|
||||||
val darkDividerColor: Color,
|
val darkDividerColor: Color,
|
||||||
val tabTextColor: Color,
|
val tabTextColor: Color,
|
||||||
val panelBackgroundColor: Color,
|
val panelBackgroundColor: Color,
|
||||||
|
val cameraDisabledBackgroundColor: Color,
|
||||||
|
val cameraDisabledFrameColor: Color,
|
||||||
val radioButtonColor: Color,
|
val radioButtonColor: Color,
|
||||||
val radioButtonTextColor: Color,
|
val radioButtonTextColor: Color,
|
||||||
val historyBackgroundColor: Color,
|
val historyBackgroundColor: Color,
|
||||||
val historyRedColor: Color,
|
val historyRedColor: Color,
|
||||||
val historySyncingColor: Color,
|
val historySyncingColor: Color,
|
||||||
|
val topAppBarColors: TopAppBarColors,
|
||||||
|
val transparentTopAppBarColors: TopAppBarColors
|
||||||
) {
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
fun surfaceGradient() =
|
fun surfaceGradient() =
|
||||||
|
|
|
@ -43,6 +43,8 @@ internal object Dark {
|
||||||
val tabTextColor = Color(0xFF040404)
|
val tabTextColor = Color(0xFF040404)
|
||||||
val layoutStroke = Color(0xFFFFFFFF)
|
val layoutStroke = Color(0xFFFFFFFF)
|
||||||
val panelBackgroundColor = Color(0xFFEAEAEA)
|
val panelBackgroundColor = Color(0xFFEAEAEA)
|
||||||
|
val cameraDisabledBackgroundColor = Color(0xFF5E5C5C)
|
||||||
|
val cameraDisabledFrameColor = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
val primaryButton = Color(0xFFFFFFFF)
|
val primaryButton = Color(0xFFFFFFFF)
|
||||||
val secondaryButton = Color(0xFFFFFFFF)
|
val secondaryButton = Color(0xFFFFFFFF)
|
||||||
|
@ -56,7 +58,6 @@ internal object Dark {
|
||||||
val circularProgressBarScreen = Color(0xFFFFFFFF)
|
val circularProgressBarScreen = Color(0xFFFFFFFF)
|
||||||
val linearProgressBarTrack = Color(0xFFD9D9D9)
|
val linearProgressBarTrack = Color(0xFFD9D9D9)
|
||||||
val linearProgressBarBackground = complementaryColor
|
val linearProgressBarBackground = complementaryColor
|
||||||
val restoringTopAppBarColor = Color(0xFF8A8888)
|
|
||||||
|
|
||||||
val callout = Color(0xFFFFFFFF)
|
val callout = Color(0xFFFFFFFF)
|
||||||
val onCallout = Color(0xFFFFFFFF)
|
val onCallout = Color(0xFFFFFFFF)
|
||||||
|
@ -74,6 +75,9 @@ internal object Dark {
|
||||||
val historyBackgroundColor = Color(0xFFF6F6F6)
|
val historyBackgroundColor = Color(0xFFF6F6F6)
|
||||||
val historyRedColor = textFieldWarning
|
val historyRedColor = textFieldWarning
|
||||||
val historySyncingColor = panelBackgroundColor
|
val historySyncingColor = panelBackgroundColor
|
||||||
|
|
||||||
|
val topAppBarColors = DarkTopAppBarColors()
|
||||||
|
val transparentTopAppBarColors = TransparentTopAppBarColors()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object Light {
|
internal object Light {
|
||||||
|
@ -105,6 +109,8 @@ internal object Light {
|
||||||
val tabTextColor = Color(0xFF040404)
|
val tabTextColor = Color(0xFF040404)
|
||||||
val layoutStroke = Color(0xFF000000)
|
val layoutStroke = Color(0xFF000000)
|
||||||
val panelBackgroundColor = Color(0xFFEAEAEA)
|
val panelBackgroundColor = Color(0xFFEAEAEA)
|
||||||
|
val cameraDisabledBackgroundColor = Color(0xFF5E5C5C)
|
||||||
|
val cameraDisabledFrameColor = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
val primaryButton = Color(0xFF000000)
|
val primaryButton = Color(0xFF000000)
|
||||||
val secondaryButton = Color(0xFFFFFFFF)
|
val secondaryButton = Color(0xFFFFFFFF)
|
||||||
|
@ -118,7 +124,6 @@ internal object Light {
|
||||||
val circularProgressBarSmallDark = textBodyOnBackground
|
val circularProgressBarSmallDark = textBodyOnBackground
|
||||||
val linearProgressBarTrack = Color(0xFFD9D9D9)
|
val linearProgressBarTrack = Color(0xFFD9D9D9)
|
||||||
val linearProgressBarBackground = complementaryColor
|
val linearProgressBarBackground = complementaryColor
|
||||||
val restoringTopAppBarColor = Color(0xFF8A8888)
|
|
||||||
|
|
||||||
val callout = Color(0xFFFFFFFF)
|
val callout = Color(0xFFFFFFFF)
|
||||||
val onCallout = Color(0xFFFFFFFF)
|
val onCallout = Color(0xFFFFFFFF)
|
||||||
|
@ -135,6 +140,9 @@ internal object Light {
|
||||||
val historyBackgroundColor = Color(0xFFF6F6F6)
|
val historyBackgroundColor = Color(0xFFF6F6F6)
|
||||||
val historyRedColor = textFieldWarning
|
val historyRedColor = textFieldWarning
|
||||||
val historySyncingColor = Dark.panelBackgroundColor
|
val historySyncingColor = Dark.panelBackgroundColor
|
||||||
|
|
||||||
|
val topAppBarColors = LightTopAppBarColors()
|
||||||
|
val transparentTopAppBarColors = TransparentTopAppBarColors()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val DarkColorPalette =
|
internal val DarkColorPalette =
|
||||||
|
@ -174,7 +182,6 @@ internal val DarkExtendedColorPalette =
|
||||||
circularProgressBarScreen = Dark.circularProgressBarScreen,
|
circularProgressBarScreen = Dark.circularProgressBarScreen,
|
||||||
linearProgressBarTrack = Dark.linearProgressBarTrack,
|
linearProgressBarTrack = Dark.linearProgressBarTrack,
|
||||||
linearProgressBarBackground = Dark.linearProgressBarBackground,
|
linearProgressBarBackground = Dark.linearProgressBarBackground,
|
||||||
restoringTopAppBarColor = Dark.restoringTopAppBarColor,
|
|
||||||
chipIndex = Dark.textChipIndex,
|
chipIndex = Dark.textChipIndex,
|
||||||
textCommon = Dark.textCommon,
|
textCommon = Dark.textCommon,
|
||||||
textMedium = Dark.textMedium,
|
textMedium = Dark.textMedium,
|
||||||
|
@ -200,11 +207,15 @@ internal val DarkExtendedColorPalette =
|
||||||
darkDividerColor = Dark.darkDividerColor,
|
darkDividerColor = Dark.darkDividerColor,
|
||||||
tabTextColor = Dark.tabTextColor,
|
tabTextColor = Dark.tabTextColor,
|
||||||
panelBackgroundColor = Dark.panelBackgroundColor,
|
panelBackgroundColor = Dark.panelBackgroundColor,
|
||||||
|
cameraDisabledBackgroundColor = Dark.cameraDisabledBackgroundColor,
|
||||||
|
cameraDisabledFrameColor = Dark.cameraDisabledFrameColor,
|
||||||
radioButtonColor = Dark.radioButtonColor,
|
radioButtonColor = Dark.radioButtonColor,
|
||||||
radioButtonTextColor = Dark.radioButtonTextColor,
|
radioButtonTextColor = Dark.radioButtonTextColor,
|
||||||
historyBackgroundColor = Dark.historyBackgroundColor,
|
historyBackgroundColor = Dark.historyBackgroundColor,
|
||||||
historyRedColor = Dark.historyRedColor,
|
historyRedColor = Dark.historyRedColor,
|
||||||
historySyncingColor = Dark.historySyncingColor,
|
historySyncingColor = Dark.historySyncingColor,
|
||||||
|
topAppBarColors = Dark.topAppBarColors,
|
||||||
|
transparentTopAppBarColors = Dark.transparentTopAppBarColors
|
||||||
)
|
)
|
||||||
|
|
||||||
internal val LightExtendedColorPalette =
|
internal val LightExtendedColorPalette =
|
||||||
|
@ -220,7 +231,6 @@ internal val LightExtendedColorPalette =
|
||||||
circularProgressBarSmallDark = Light.circularProgressBarSmallDark,
|
circularProgressBarSmallDark = Light.circularProgressBarSmallDark,
|
||||||
linearProgressBarTrack = Light.linearProgressBarTrack,
|
linearProgressBarTrack = Light.linearProgressBarTrack,
|
||||||
linearProgressBarBackground = Light.linearProgressBarBackground,
|
linearProgressBarBackground = Light.linearProgressBarBackground,
|
||||||
restoringTopAppBarColor = Light.restoringTopAppBarColor,
|
|
||||||
chipIndex = Light.textChipIndex,
|
chipIndex = Light.textChipIndex,
|
||||||
textCommon = Light.textCommon,
|
textCommon = Light.textCommon,
|
||||||
textMedium = Light.textMedium,
|
textMedium = Light.textMedium,
|
||||||
|
@ -246,11 +256,15 @@ internal val LightExtendedColorPalette =
|
||||||
darkDividerColor = Light.darkDividerColor,
|
darkDividerColor = Light.darkDividerColor,
|
||||||
tabTextColor = Light.tabTextColor,
|
tabTextColor = Light.tabTextColor,
|
||||||
panelBackgroundColor = Light.panelBackgroundColor,
|
panelBackgroundColor = Light.panelBackgroundColor,
|
||||||
|
cameraDisabledBackgroundColor = Light.cameraDisabledBackgroundColor,
|
||||||
|
cameraDisabledFrameColor = Light.cameraDisabledFrameColor,
|
||||||
radioButtonColor = Light.radioButtonColor,
|
radioButtonColor = Light.radioButtonColor,
|
||||||
radioButtonTextColor = Light.radioButtonTextColor,
|
radioButtonTextColor = Light.radioButtonTextColor,
|
||||||
historyBackgroundColor = Light.historyBackgroundColor,
|
historyBackgroundColor = Light.historyBackgroundColor,
|
||||||
historyRedColor = Light.historyRedColor,
|
historyRedColor = Light.historyRedColor,
|
||||||
historySyncingColor = Light.historySyncingColor,
|
historySyncingColor = Light.historySyncingColor,
|
||||||
|
topAppBarColors = Light.topAppBarColors,
|
||||||
|
transparentTopAppBarColors = Light.transparentTopAppBarColors
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("CompositionLocalAllowlist")
|
@Suppress("CompositionLocalAllowlist")
|
||||||
|
@ -268,7 +282,6 @@ internal val LocalExtendedColors =
|
||||||
circularProgressBarSmallDark = Color.Unspecified,
|
circularProgressBarSmallDark = Color.Unspecified,
|
||||||
linearProgressBarTrack = Color.Unspecified,
|
linearProgressBarTrack = Color.Unspecified,
|
||||||
linearProgressBarBackground = Color.Unspecified,
|
linearProgressBarBackground = Color.Unspecified,
|
||||||
restoringTopAppBarColor = Color.Unspecified,
|
|
||||||
chipIndex = Color.Unspecified,
|
chipIndex = Color.Unspecified,
|
||||||
textCommon = Color.Unspecified,
|
textCommon = Color.Unspecified,
|
||||||
textMedium = Color.Unspecified,
|
textMedium = Color.Unspecified,
|
||||||
|
@ -294,10 +307,14 @@ internal val LocalExtendedColors =
|
||||||
darkDividerColor = Color.Unspecified,
|
darkDividerColor = Color.Unspecified,
|
||||||
tabTextColor = Color.Unspecified,
|
tabTextColor = Color.Unspecified,
|
||||||
panelBackgroundColor = Color.Unspecified,
|
panelBackgroundColor = Color.Unspecified,
|
||||||
|
cameraDisabledBackgroundColor = Color.Unspecified,
|
||||||
|
cameraDisabledFrameColor = Color.Unspecified,
|
||||||
radioButtonColor = Color.Unspecified,
|
radioButtonColor = Color.Unspecified,
|
||||||
radioButtonTextColor = Color.Unspecified,
|
radioButtonTextColor = Color.Unspecified,
|
||||||
historyBackgroundColor = Color.Unspecified,
|
historyBackgroundColor = Color.Unspecified,
|
||||||
historyRedColor = Color.Unspecified,
|
historyRedColor = Color.Unspecified,
|
||||||
historySyncingColor = Color.Unspecified,
|
historySyncingColor = Color.Unspecified,
|
||||||
|
topAppBarColors = DefaultTopAppBarColors(),
|
||||||
|
transparentTopAppBarColors = DefaultTopAppBarColors(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package co.electriccoin.zcash.ui.design.theme.internal
|
||||||
|
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
interface TopAppBarColors {
|
||||||
|
val containerColor: Color
|
||||||
|
val navigationColor: Color
|
||||||
|
val titleColor: Color
|
||||||
|
val subTitleColor: Color
|
||||||
|
val actionColor: Color
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun toMaterialTopAppBarColors() =
|
||||||
|
androidx.compose.material3.TopAppBarColors(
|
||||||
|
containerColor = containerColor,
|
||||||
|
scrolledContainerColor = containerColor,
|
||||||
|
navigationIconContentColor = navigationColor,
|
||||||
|
titleContentColor = titleColor,
|
||||||
|
actionIconContentColor = actionColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal data class DefaultTopAppBarColors(
|
||||||
|
override val containerColor: Color = Color.Unspecified,
|
||||||
|
override val navigationColor: Color = Color.Unspecified,
|
||||||
|
override val titleColor: Color = Color.Unspecified,
|
||||||
|
override val subTitleColor: Color = Color.Unspecified,
|
||||||
|
override val actionColor: Color = Color.Unspecified,
|
||||||
|
) : TopAppBarColors
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal data class LightTopAppBarColors(
|
||||||
|
override val containerColor: Color = Color(0xFFFFFFFF),
|
||||||
|
override val navigationColor: Color = Color(0xFF000000),
|
||||||
|
override val titleColor: Color = Color(0xFF000000),
|
||||||
|
override val subTitleColor: Color = Color(0xFF8A8888),
|
||||||
|
override val actionColor: Color = Color(0xFF000000),
|
||||||
|
) : TopAppBarColors
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal data class DarkTopAppBarColors(
|
||||||
|
override val containerColor: Color = Color(0xFF000000),
|
||||||
|
override val navigationColor: Color = Color(0xFFFFFFFF),
|
||||||
|
override val titleColor: Color = Color(0xFFFFFFFF),
|
||||||
|
override val subTitleColor: Color = Color(0xFF8A8888),
|
||||||
|
override val actionColor: Color = Color(0xFFFFFFFF),
|
||||||
|
) : TopAppBarColors
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal data class TransparentTopAppBarColors(
|
||||||
|
override val containerColor: Color = Color(0x00000000),
|
||||||
|
override val navigationColor: Color = Color(0xFFFFFFFF),
|
||||||
|
override val titleColor: Color = Color(0xFFFFFFFF),
|
||||||
|
override val subTitleColor: Color = Color(0xFFFFFFFF),
|
||||||
|
override val actionColor: Color = Color(0xFFFFFFFF),
|
||||||
|
) : TopAppBarColors
|
|
@ -57,6 +57,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.zcash.sdk)
|
||||||
|
|
||||||
implementation(projects.uiLib)
|
implementation(projects.uiLib)
|
||||||
implementation(projects.uiDesignLib)
|
implementation(projects.uiDesignLib)
|
||||||
implementation(projects.testLib)
|
implementation(projects.testLib)
|
||||||
|
|
|
@ -2,9 +2,7 @@ package co.electriccoin.zcash.ui.integration.test.screen.scan.view
|
||||||
|
|
||||||
import androidx.compose.ui.test.assertHasClickAction
|
import androidx.compose.ui.test.assertHasClickAction
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.assertTextEquals
|
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
@ -14,6 +12,7 @@ import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity
|
import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.getStringResource
|
import co.electriccoin.zcash.ui.integration.test.common.getStringResource
|
||||||
|
import co.electriccoin.zcash.ui.integration.test.common.getStringResourceWithArgs
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.waitForDeviceIdle
|
import co.electriccoin.zcash.ui.integration.test.common.waitForDeviceIdle
|
||||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
||||||
|
@ -71,21 +70,10 @@ class ScanViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
testSetup.grantPermission()
|
testSetup.grantPermission()
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription(
|
composeTestRule.onNodeWithText(getStringResource(R.string.scan_cancel_button).uppercase()).also {
|
||||||
getStringResource(R.string.scan_back_content_description)
|
|
||||||
).also {
|
|
||||||
it.assertIsDisplayed()
|
it.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.scan_hint)).also {
|
|
||||||
it.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(ScanTag.TEXT_STATE).also {
|
|
||||||
it.assertIsDisplayed()
|
|
||||||
it.assertTextEquals(getStringResource(R.string.scan_state_scanning))
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(ScanTag.QR_FRAME).also {
|
composeTestRule.onNodeWithTag(ScanTag.QR_FRAME).also {
|
||||||
it.assertIsDisplayed()
|
it.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
@ -109,23 +97,19 @@ class ScanViewTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
assertEquals(ScanState.Permission, testSetup.getScanState())
|
assertEquals(ScanState.Permission, testSetup.getScanState())
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(ScanTag.QR_FRAME).also {
|
|
||||||
it.assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(ScanTag.CAMERA_VIEW).also {
|
composeTestRule.onNodeWithTag(ScanTag.CAMERA_VIEW).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.scan_hint)).also {
|
composeTestRule.onNodeWithText(
|
||||||
|
getStringResourceWithArgs(
|
||||||
|
resId = R.string.scan_state_permission,
|
||||||
|
getStringResource(R.string.app_name)
|
||||||
|
)
|
||||||
|
).also {
|
||||||
it.assertIsDisplayed()
|
it.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(ScanTag.TEXT_STATE).also {
|
|
||||||
it.assertIsDisplayed()
|
|
||||||
it.assertTextEquals(getStringResource(R.string.scan_state_permission))
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.scan_settings_button), ignoreCase = true).also {
|
composeTestRule.onNodeWithText(getStringResource(R.string.scan_settings_button), ignoreCase = true).also {
|
||||||
it.assertIsDisplayed()
|
it.assertIsDisplayed()
|
||||||
it.assertHasClickAction()
|
it.assertHasClickAction()
|
||||||
|
|
|
@ -3,6 +3,8 @@ package co.electriccoin.zcash.ui.integration.test.screen.scan.view
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionNegativeButtonUiObject
|
import co.electriccoin.zcash.ui.integration.test.common.getPermissionNegativeButtonUiObject
|
||||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
||||||
|
@ -56,7 +58,9 @@ class ScanViewTestSetup(
|
||||||
},
|
},
|
||||||
onScanStateChanged = {
|
onScanStateChanged = {
|
||||||
scanState.set(it)
|
scanState.set(it)
|
||||||
}
|
},
|
||||||
|
walletRestoringState = WalletRestoringState.NONE,
|
||||||
|
addressValidationResult = AddressType.Unified
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.screen.scan.view
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.assertTextEquals
|
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
@ -33,12 +32,12 @@ class ScanViewBasicTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@MediumTest
|
@MediumTest
|
||||||
fun back() {
|
fun cancel() {
|
||||||
val testSetup = newTestSetup()
|
val testSetup = newTestSetup()
|
||||||
|
|
||||||
assertEquals(0, testSetup.getOnBackCount())
|
assertEquals(0, testSetup.getOnBackCount())
|
||||||
|
|
||||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.scan_back_content_description)).also {
|
composeTestRule.onNodeWithText(getStringResource(R.string.scan_cancel_button).uppercase()).also {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +51,7 @@ class ScanViewBasicTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
// Permission granted ui items (visible):
|
// Permission granted ui items (visible):
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.scan_header)).also {
|
composeTestRule.onNodeWithText(getStringResource(R.string.scan_cancel_button).uppercase()).also {
|
||||||
it.assertIsDisplayed()
|
it.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,12 +61,7 @@ class ScanViewBasicTest : UiTestPrerequisites() {
|
||||||
it.assertIsDisplayed()
|
it.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(ScanTag.TEXT_STATE).also {
|
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.scan_torch_content_description)).also {
|
||||||
it.assertIsDisplayed()
|
|
||||||
it.assertTextEquals(getStringResource(R.string.scan_state_scanning))
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithText(getStringResource(R.string.scan_hint)).also {
|
|
||||||
it.assertIsDisplayed()
|
it.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ package co.electriccoin.zcash.ui.screen.scan.view
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
@ -36,7 +38,9 @@ class ScanViewBasicTestSetup(
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
onScanStateChanged = {
|
onScanStateChanged = {
|
||||||
scanState.set(it)
|
scanState.set(it)
|
||||||
}
|
},
|
||||||
|
walletRestoringState = WalletRestoringState.NONE,
|
||||||
|
addressValidationResult = AddressType.Shielded
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
package co.electriccoin.zcash.ui.screen.scan
|
package co.electriccoin.zcash.ui.screen.scan
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import android.content.Context
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
import co.electriccoin.zcash.ui.MainActivity
|
import co.electriccoin.zcash.ui.MainActivity
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
import co.electriccoin.zcash.ui.common.model.SerializableAddress
|
||||||
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||||
import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil
|
import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil
|
||||||
|
@ -21,25 +27,34 @@ internal fun MainActivity.WrapScanValidator(
|
||||||
onScanValid: (address: SerializableAddress) -> Unit,
|
onScanValid: (address: SerializableAddress) -> Unit,
|
||||||
goBack: () -> Unit
|
goBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val walletViewModel by viewModels<WalletViewModel>()
|
||||||
|
|
||||||
|
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
||||||
|
|
||||||
|
val walletRestoringState = walletViewModel.walletRestoringState.collectAsStateWithLifecycle().value
|
||||||
|
|
||||||
WrapScan(
|
WrapScan(
|
||||||
this,
|
context = this,
|
||||||
onScanValid = onScanValid,
|
onScanValid = onScanValid,
|
||||||
goBack = goBack
|
goBack = goBack,
|
||||||
|
synchronizer = synchronizer,
|
||||||
|
walletRestoringState = walletRestoringState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WrapScan(
|
fun WrapScan(
|
||||||
activity: ComponentActivity,
|
context: Context,
|
||||||
|
goBack: () -> Unit,
|
||||||
onScanValid: (address: SerializableAddress) -> Unit,
|
onScanValid: (address: SerializableAddress) -> Unit,
|
||||||
goBack: () -> Unit
|
synchronizer: Synchronizer?,
|
||||||
|
walletRestoringState: WalletRestoringState,
|
||||||
) {
|
) {
|
||||||
val walletViewModel by activity.viewModels<WalletViewModel>()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
var addressValidationResult by remember { mutableStateOf<AddressType?>(null) }
|
||||||
|
|
||||||
if (synchronizer == null) {
|
if (synchronizer == null) {
|
||||||
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
|
||||||
|
@ -49,34 +64,32 @@ fun WrapScan(
|
||||||
} else {
|
} else {
|
||||||
Scan(
|
Scan(
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
addressValidationResult = addressValidationResult,
|
||||||
onBack = goBack,
|
onBack = goBack,
|
||||||
onScanned = { result ->
|
onScanned = { result ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val addressType = synchronizer.validateAddress(result)
|
addressValidationResult = synchronizer.validateAddress(result)
|
||||||
val isAddressValid = !addressType.isNotValid
|
val isAddressValid = addressValidationResult?.let { !it.isNotValid } ?: false
|
||||||
if (isAddressValid) {
|
if (isAddressValid) {
|
||||||
onScanValid(SerializableAddress(result, addressType))
|
onScanValid(SerializableAddress(result, addressValidationResult!!))
|
||||||
} else {
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = activity.getString(R.string.scan_validation_invalid_address)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOpenSettings = {
|
onOpenSettings = {
|
||||||
runCatching {
|
runCatching {
|
||||||
activity.startActivity(SettingsUtil.newSettingsIntent(activity.packageName))
|
context.startActivity(SettingsUtil.newSettingsIntent(context.packageName))
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
// This case should not really happen, as the Settings app should be available on every
|
// This case should not really happen, as the Settings app should be available on every
|
||||||
// Android device, but we need to handle it somehow.
|
// Android device, but we need to handle it somehow.
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
message = activity.getString(R.string.scan_settings_open_failed)
|
message = context.getString(R.string.scan_settings_open_failed)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onScanStateChanged = {}
|
onScanStateChanged = {},
|
||||||
|
walletRestoringState = walletRestoringState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ package co.electriccoin.zcash.ui.screen.scan
|
||||||
* These are only used for automated testing.
|
* These are only used for automated testing.
|
||||||
*/
|
*/
|
||||||
object ScanTag {
|
object ScanTag {
|
||||||
const val TEXT_STATE = "text_state"
|
const val FAILED_TEXT_STATE = "failed_text_state"
|
||||||
const val CAMERA_VIEW = "camera_view"
|
const val CAMERA_VIEW = "camera_view"
|
||||||
const val QR_FRAME = "frame"
|
const val QR_FRAME = "frame"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package co.electriccoin.zcash.ui.screen.scan.util
|
||||||
import android.graphics.ImageFormat
|
import android.graphics.ImageFormat
|
||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.view.FramePosition
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
import com.google.zxing.BinaryBitmap
|
import com.google.zxing.BinaryBitmap
|
||||||
import com.google.zxing.DecodeHintType
|
import com.google.zxing.DecodeHintType
|
||||||
|
@ -11,9 +13,9 @@ import com.google.zxing.PlanarYUVLuminanceSource
|
||||||
import com.google.zxing.common.HybridBinarizer
|
import com.google.zxing.common.HybridBinarizer
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
// TODO [#437]: https://github.com/Electric-Coin-Company/zashi-android/issues/437
|
|
||||||
class QrCodeAnalyzer(
|
class QrCodeAnalyzer(
|
||||||
private val onQrCodeScanned: (String) -> Unit
|
private val framePosition: FramePosition,
|
||||||
|
private val onQrCodeScanned: (String) -> Unit,
|
||||||
) : ImageAnalysis.Analyzer {
|
) : ImageAnalysis.Analyzer {
|
||||||
private val supportedImageFormats =
|
private val supportedImageFormats =
|
||||||
listOf(
|
listOf(
|
||||||
|
@ -26,6 +28,15 @@ class QrCodeAnalyzer(
|
||||||
image.use {
|
image.use {
|
||||||
if (image.format in supportedImageFormats) {
|
if (image.format in supportedImageFormats) {
|
||||||
val bytes = image.planes.first().buffer.toByteArray()
|
val bytes = image.planes.first().buffer.toByteArray()
|
||||||
|
|
||||||
|
Twig.debug {
|
||||||
|
"Scan result: " +
|
||||||
|
"Frame: $framePosition, " +
|
||||||
|
"Info: ${image.imageInfo}, " +
|
||||||
|
"Image width: ${image.width}, " +
|
||||||
|
"Image height: ${image.height}"
|
||||||
|
}
|
||||||
|
|
||||||
val source =
|
val source =
|
||||||
PlanarYUVLuminanceSource(
|
PlanarYUVLuminanceSource(
|
||||||
bytes,
|
bytes,
|
||||||
|
@ -40,6 +51,23 @@ class QrCodeAnalyzer(
|
||||||
|
|
||||||
val binaryBmp = BinaryBitmap(HybridBinarizer(source))
|
val binaryBmp = BinaryBitmap(HybridBinarizer(source))
|
||||||
|
|
||||||
|
// TODO [#1380]: Leverage FramePosition in QrCodeAnalyzer
|
||||||
|
// TODO [#1380]: https://github.com/Electric-Coin-Company/zashi-android/issues/1380
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
val binaryBitmapCropped =
|
||||||
|
binaryBmp.crop(
|
||||||
|
(binaryBmp.width * 0.25).toInt(),
|
||||||
|
(binaryBmp.height * 0.30).toInt(),
|
||||||
|
(binaryBmp.width * 0.30).toInt(),
|
||||||
|
(binaryBmp.height * 0.4).toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
Twig.debug {
|
||||||
|
"Scan result cropped: " +
|
||||||
|
"Image width: ${binaryBitmapCropped.width}, " +
|
||||||
|
"Image height: ${binaryBitmapCropped.height}"
|
||||||
|
}
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val result =
|
val result =
|
||||||
MultiFormatReader().apply {
|
MultiFormatReader().apply {
|
||||||
|
@ -51,7 +79,7 @@ class QrCodeAnalyzer(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}.decode(binaryBmp)
|
}.decode(binaryBitmapCropped)
|
||||||
|
|
||||||
onQrCodeScanned(result.text)
|
onQrCodeScanned(result.text)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
|
|
@ -2,45 +2,44 @@ package co.electriccoin.zcash.ui.screen.scan.view
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.camera.core.CameraControl
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.ClipOp
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
@ -48,6 +47,8 @@ import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -55,12 +56,14 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import androidx.constraintlayout.compose.Dimension
|
import androidx.constraintlayout.compose.Dimension
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
import co.electriccoin.zcash.spackle.Twig
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.design.component.Body
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||||
import co.electriccoin.zcash.ui.design.component.SecondaryButton
|
import co.electriccoin.zcash.ui.design.component.SecondaryButton
|
||||||
import co.electriccoin.zcash.ui.design.component.Small
|
import co.electriccoin.zcash.ui.design.component.Small
|
||||||
|
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
import co.electriccoin.zcash.ui.screen.scan.model.ScanState
|
||||||
|
@ -77,8 +80,6 @@ import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.guava.await
|
import kotlinx.coroutines.guava.await
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
// TODO [#423]: QR scan screen elements transparency
|
|
||||||
// TODO [#423]: https://github.com/Electric-Coin-Company/zashi-android/issues/423
|
|
||||||
@Preview("Scan")
|
@Preview("Scan")
|
||||||
@Composable
|
@Composable
|
||||||
private fun PreviewScan() {
|
private fun PreviewScan() {
|
||||||
|
@ -89,111 +90,26 @@ private fun PreviewScan() {
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onScanned = {},
|
onScanned = {},
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
onScanStateChanged = {}
|
onScanStateChanged = {},
|
||||||
|
walletRestoringState = WalletRestoringState.NONE,
|
||||||
|
addressValidationResult = AddressType.Transparent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
@Suppress("LongParameterList", "UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
fun Scan(
|
fun Scan(
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onScanned: (String) -> Unit,
|
onScanned: (String) -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onScanStateChanged: (ScanState) -> Unit
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = { ScanTopAppBar(onBack = onBack) },
|
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
|
||||||
) { paddingValues ->
|
|
||||||
ScanMainContent(
|
|
||||||
onScanned,
|
|
||||||
onOpenSettings,
|
|
||||||
onBack,
|
|
||||||
onScanStateChanged,
|
|
||||||
snackbarHostState,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color.Black)
|
|
||||||
.padding(
|
|
||||||
top = paddingValues.calculateTopPadding(),
|
|
||||||
bottom = paddingValues.calculateBottomPadding(),
|
|
||||||
start = ZcashTheme.dimens.spacingNone,
|
|
||||||
end = ZcashTheme.dimens.spacingNone
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ScanBottomItems(
|
|
||||||
scanState: ScanState,
|
|
||||||
onOpenSettings: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(modifier) {
|
|
||||||
Body(
|
|
||||||
text = stringResource(id = R.string.scan_hint),
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
|
||||||
|
|
||||||
Small(
|
|
||||||
text =
|
|
||||||
when (scanState) {
|
|
||||||
ScanState.Permission -> stringResource(id = R.string.scan_state_permission)
|
|
||||||
ScanState.Scanning -> stringResource(id = R.string.scan_state_scanning)
|
|
||||||
ScanState.Failed -> stringResource(id = R.string.scan_state_failed)
|
|
||||||
},
|
|
||||||
color = Color.White,
|
|
||||||
modifier = Modifier.testTag(ScanTag.TEXT_STATE)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (scanState == ScanState.Permission) {
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
|
||||||
|
|
||||||
SecondaryButton(
|
|
||||||
onClick = onOpenSettings,
|
|
||||||
text = stringResource(id = R.string.scan_settings_button)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
private fun ScanTopAppBar(onBack: () -> Unit) {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(text = stringResource(id = R.string.scan_header)) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.scan_back_content_description)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Suppress("LongMethod", "LongParameterList")
|
|
||||||
@Composable
|
|
||||||
private fun ScanMainContent(
|
|
||||||
onScanned: (String) -> Unit,
|
|
||||||
onOpenSettings: () -> Unit,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onScanStateChanged: (ScanState) -> Unit,
|
onScanStateChanged: (ScanState) -> Unit,
|
||||||
snackbarHostState: SnackbarHostState,
|
walletRestoringState: WalletRestoringState,
|
||||||
modifier: Modifier = Modifier
|
addressValidationResult: AddressType?
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val permissionState =
|
val permissionState =
|
||||||
rememberPermissionState(
|
rememberPermissionState(
|
||||||
Manifest.permission.CAMERA
|
Manifest.permission.CAMERA
|
||||||
|
@ -210,102 +126,297 @@ private fun ScanMainContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permissionState.status.isGranted) {
|
Scaffold(
|
||||||
setScanState(ScanState.Permission)
|
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||||
if (permissionState.status.shouldShowRationale) {
|
) { _ ->
|
||||||
// keep blank screen with a link to the app settings
|
Box {
|
||||||
// user denied the permission previously
|
ScanMainContent(
|
||||||
} else {
|
addressValidationResult = addressValidationResult,
|
||||||
LaunchedEffect(key1 = true) {
|
onScanned = onScanned,
|
||||||
permissionState.launchPermissionRequest()
|
onOpenSettings = onOpenSettings,
|
||||||
|
onBack = onBack,
|
||||||
|
onScanStateChanged = onScanStateChanged,
|
||||||
|
permissionState = permissionState,
|
||||||
|
scanState = scanState,
|
||||||
|
setScanState = setScanState,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
if (scanState != ScanState.Scanning) {
|
||||||
|
ZcashTheme.colors.cameraDisabledBackgroundColor
|
||||||
|
} else {
|
||||||
|
Color.Black
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Intentionally omitting paddingValues to have edge to edge design
|
||||||
|
)
|
||||||
|
|
||||||
|
ScanTopAppBar(
|
||||||
|
onBack = onBack,
|
||||||
|
showBack = scanState != ScanState.Scanning,
|
||||||
|
showRestoring = walletRestoringState == WalletRestoringState.RESTORING,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScanBottomItems(
|
||||||
|
addressValidationResult: AddressType?,
|
||||||
|
scanState: ScanState,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(modifier) {
|
||||||
|
var failureText: String? = null
|
||||||
|
|
||||||
|
// Check validation result, if any
|
||||||
|
if (addressValidationResult is AddressType.Invalid) {
|
||||||
|
failureText = stringResource(id = R.string.scan_address_validation_failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission request result, if any
|
||||||
|
failureText =
|
||||||
|
when (scanState) {
|
||||||
|
ScanState.Permission ->
|
||||||
|
stringResource(
|
||||||
|
id = R.string.scan_state_permission,
|
||||||
|
stringResource(id = R.string.app_name)
|
||||||
|
)
|
||||||
|
ScanState.Failed -> stringResource(id = R.string.scan_state_failed)
|
||||||
|
ScanState.Scanning -> failureText
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failureText != null) {
|
||||||
|
Small(
|
||||||
|
text = failureText,
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(ScanTag.FAILED_TEXT_STATE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||||
|
|
||||||
|
when (scanState) {
|
||||||
|
ScanState.Scanning, ScanState.Failed -> {
|
||||||
|
SecondaryButton(
|
||||||
|
onClick = onBack,
|
||||||
|
text = stringResource(id = R.string.scan_cancel_button)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ScanState.Permission -> {
|
||||||
|
SecondaryButton(
|
||||||
|
onClick = onOpenSettings,
|
||||||
|
text = stringResource(id = R.string.scan_settings_button)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (scanState == ScanState.Failed) {
|
|
||||||
// keep current state
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
} else if (permissionState.status.isGranted) {
|
}
|
||||||
if (scanState != ScanState.Scanning) {
|
}
|
||||||
setScanState(ScanState.Scanning)
|
|
||||||
|
@Composable
|
||||||
|
private fun ScanTopAppBar(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
showBack: Boolean,
|
||||||
|
showRestoring: Boolean,
|
||||||
|
) {
|
||||||
|
SmallTopAppBar(
|
||||||
|
restoringLabel =
|
||||||
|
if (showRestoring) {
|
||||||
|
stringResource(id = R.string.restoring_wallet_label)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
backText =
|
||||||
|
if (showBack) {
|
||||||
|
stringResource(id = R.string.scan_back)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
backContentDescriptionText = stringResource(id = R.string.scan_back_content_description),
|
||||||
|
colors = ZcashTheme.colors.transparentTopAppBarColors,
|
||||||
|
onBack = onBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const val CAMERA_TRANSLUCENT_BORDER = 0.5f
|
||||||
|
|
||||||
|
const val FRAME_SIZE_RATIO = 0.6f
|
||||||
|
|
||||||
|
data class FramePosition(
|
||||||
|
val left: Float,
|
||||||
|
val top: Float,
|
||||||
|
val right: Float,
|
||||||
|
val bottom: Float,
|
||||||
|
val screenHeight: Int,
|
||||||
|
val screenWidth: Int
|
||||||
|
) {
|
||||||
|
val width: Float = right - left
|
||||||
|
val height: Float = bottom - top
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Suppress("LongMethod", "LongParameterList")
|
||||||
|
@Composable
|
||||||
|
private fun ScanMainContent(
|
||||||
|
addressValidationResult: AddressType?,
|
||||||
|
onScanned: (String) -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onScanStateChanged: (ScanState) -> Unit,
|
||||||
|
permissionState: PermissionState,
|
||||||
|
scanState: ScanState,
|
||||||
|
setScanState: (ScanState) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
(!permissionState.status.isGranted) -> {
|
||||||
|
setScanState(ScanState.Permission)
|
||||||
|
if (permissionState.status.shouldShowRationale) {
|
||||||
|
// Keep dark screen with a link to the app settings - user denied the permission previously
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(key1 = true) {
|
||||||
|
permissionState.launchPermissionRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(scanState == ScanState.Failed) -> {
|
||||||
|
// Keep current state
|
||||||
|
}
|
||||||
|
(permissionState.status.isGranted) -> {
|
||||||
|
if (scanState != ScanState.Scanning) {
|
||||||
|
setScanState(ScanState.Scanning)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we calculate the best frame size for the current device screen
|
// Calculate the best frame size for the current device screen
|
||||||
val framePossibleSize = remember { mutableStateOf(IntSize.Zero) }
|
val framePossibleSize = remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
|
||||||
|
val frameActualSize = (framePossibleSize.value.width * FRAME_SIZE_RATIO).roundToInt()
|
||||||
|
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
val framePosition =
|
||||||
val frameActualSize =
|
FramePosition(
|
||||||
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
left = (framePossibleSize.value.width - frameActualSize) / 2f,
|
||||||
(framePossibleSize.value.height * 0.85).roundToInt()
|
top = (framePossibleSize.value.height - frameActualSize) / 2f,
|
||||||
} else {
|
right = (framePossibleSize.value.width - frameActualSize) / 2f + frameActualSize,
|
||||||
(framePossibleSize.value.width * 0.7).roundToInt()
|
bottom = (framePossibleSize.value.height - frameActualSize) / 2f + frameActualSize,
|
||||||
}
|
screenHeight = with(density) { configuration.screenHeightDp.dp.roundToPx() },
|
||||||
|
screenWidth = with(density) { configuration.screenWidthDp.dp.roundToPx() }
|
||||||
|
)
|
||||||
|
|
||||||
ConstraintLayout(modifier) {
|
val (isTorchOn, setIsTorchOn) = rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ConstraintLayout(modifier = modifier) {
|
||||||
val (frame, bottomItems) = createRefs()
|
val (frame, bottomItems) = createRefs()
|
||||||
|
|
||||||
when (scanState) {
|
when (scanState) {
|
||||||
ScanState.Permission -> {
|
ScanState.Permission -> {
|
||||||
// keep initial ui state
|
// Keep initial ui state
|
||||||
onScanStateChanged(ScanState.Permission)
|
onScanStateChanged(ScanState.Permission)
|
||||||
}
|
}
|
||||||
ScanState.Scanning -> {
|
ScanState.Scanning -> {
|
||||||
// TODO [#437]: Scan QR Screen Frame Analysing
|
|
||||||
// TODO [#437]: https://github.com/Electric-Coin-Company/zashi-android/issues/437
|
|
||||||
onScanStateChanged(ScanState.Scanning)
|
onScanStateChanged(ScanState.Scanning)
|
||||||
|
|
||||||
ScanCameraView(
|
ScanCameraView(
|
||||||
|
framePosition = framePosition,
|
||||||
|
isTorchOn = isTorchOn,
|
||||||
onScanned = onScanned,
|
onScanned = onScanned,
|
||||||
|
permissionState = permissionState,
|
||||||
setScanState = setScanState,
|
setScanState = setScanState,
|
||||||
permissionState = permissionState
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
clipRect(
|
||||||
|
clipOp = ClipOp.Difference,
|
||||||
|
left = framePosition.left,
|
||||||
|
top = framePosition.top,
|
||||||
|
right = framePosition.right,
|
||||||
|
bottom = framePosition.bottom,
|
||||||
|
) {
|
||||||
|
drawRect(Color.Black.copy(alpha = CAMERA_TRANSLUCENT_BORDER))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
imageVector =
|
||||||
|
if (isTorchOn) {
|
||||||
|
ImageVector.vectorResource(R.drawable.ic_torch_off)
|
||||||
|
} else {
|
||||||
|
ImageVector.vectorResource(R.drawable.ic_torch_on)
|
||||||
|
},
|
||||||
|
contentDescription = stringResource(id = R.string.scan_torch_content_description),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.constrainAs(frame) {
|
.offset(
|
||||||
top.linkTo(parent.top)
|
x =
|
||||||
bottom.linkTo(bottomItems.top)
|
with(density) {
|
||||||
start.linkTo(parent.start)
|
(
|
||||||
end.linkTo(parent.end)
|
framePosition.right.toDp() -
|
||||||
width = Dimension.fillToConstraints
|
ZcashTheme.dimens.cameraTorchButton -
|
||||||
height = Dimension.fillToConstraints
|
ZcashTheme.dimens.spacingDefault
|
||||||
}
|
)
|
||||||
.onSizeChanged { coordinates ->
|
},
|
||||||
framePossibleSize.value = coordinates
|
y = with(density) { framePosition.bottom.toDp() }
|
||||||
},
|
)
|
||||||
contentAlignment = Alignment.Center
|
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
||||||
) {
|
.clickable { setIsTorchOn(!isTorchOn) }
|
||||||
ScanFrame(frameActualSize)
|
.padding(ZcashTheme.dimens.spacingDefault)
|
||||||
}
|
.size(
|
||||||
|
width = ZcashTheme.dimens.cameraTorchButton,
|
||||||
|
height = ZcashTheme.dimens.cameraTorchButton
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ScanState.Failed -> {
|
ScanState.Failed -> {
|
||||||
onScanStateChanged(ScanState.Failed)
|
onScanStateChanged(ScanState.Failed)
|
||||||
|
|
||||||
// Using [rememberUpdatedState] to ensure that always the latest lambda is captured
|
|
||||||
// And to avoid Detekt warning: Lambda parameters in a @Composable that are referenced directly inside
|
|
||||||
// of restarting effects can cause issues or unpredictable behavior.
|
|
||||||
val currentOnScanStateChanged = rememberUpdatedState(newValue = onScanStateChanged)
|
|
||||||
val currentOnBack = rememberUpdatedState(newValue = onBack)
|
|
||||||
|
|
||||||
LaunchedEffect(currentOnScanStateChanged, currentOnBack) {
|
|
||||||
setScanState(ScanState.Failed)
|
|
||||||
currentOnScanStateChanged.value(ScanState.Failed)
|
|
||||||
val snackbarResult =
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = context.getString(R.string.scan_setup_failed),
|
|
||||||
actionLabel = context.getString(R.string.scan_setup_back)
|
|
||||||
)
|
|
||||||
if (snackbarResult == SnackbarResult.ActionPerformed) {
|
|
||||||
currentOnBack.value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.constrainAs(bottomItems) { bottom.linkTo(parent.bottom) }) {
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.constrainAs(frame) {
|
||||||
|
top.linkTo(parent.top)
|
||||||
|
bottom.linkTo(bottomItems.top)
|
||||||
|
start.linkTo(parent.start)
|
||||||
|
end.linkTo(parent.end)
|
||||||
|
width = Dimension.fillToConstraints
|
||||||
|
height = Dimension.fillToConstraints
|
||||||
|
}
|
||||||
|
.onSizeChanged { coordinates ->
|
||||||
|
framePossibleSize.value = coordinates
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ScanFrame(
|
||||||
|
frameSize = frameActualSize,
|
||||||
|
isScanning = scanState == ScanState.Scanning
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.constrainAs(bottomItems) { bottom.linkTo(parent.bottom) }
|
||||||
|
) {
|
||||||
ScanBottomItems(
|
ScanBottomItems(
|
||||||
scanState = scanState,
|
addressValidationResult = addressValidationResult,
|
||||||
|
onBack = onBack,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
|
scanState = scanState,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -318,31 +429,82 @@ private fun ScanMainContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanFrame(frameSize: Int) {
|
fun ScanFrame(
|
||||||
|
frameSize: Int,
|
||||||
|
isScanning: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
@Suppress("MagicNumber")
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
modifier
|
||||||
.size(with(LocalDensity.current) { frameSize.toDp() })
|
.then(
|
||||||
.background(Color.Transparent)
|
Modifier
|
||||||
.border(BorderStroke(10.dp, Color.White), RoundedCornerShape(10))
|
.size(with(LocalDensity.current) { frameSize.toDp() })
|
||||||
.testTag(ScanTag.QR_FRAME)
|
.background(
|
||||||
)
|
if (isScanning) {
|
||||||
|
Color.Transparent
|
||||||
|
} else {
|
||||||
|
ZcashTheme.colors.cameraDisabledFrameColor
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.testTag(ScanTag.QR_FRAME)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_scan_corner),
|
||||||
|
tint = Color.White,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.rotate(0f)
|
||||||
|
.align(Alignment.TopStart),
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_scan_corner),
|
||||||
|
tint = Color.White,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.rotate(90f)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_scan_corner),
|
||||||
|
tint = Color.White,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.rotate(-90f)
|
||||||
|
.align(Alignment.BottomStart),
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_scan_corner),
|
||||||
|
tint = Color.White,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.rotate(180f)
|
||||||
|
.align(Alignment.BottomEnd),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@SuppressWarnings("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanCameraView(
|
fun ScanCameraView(
|
||||||
|
framePosition: FramePosition,
|
||||||
|
isTorchOn: Boolean,
|
||||||
onScanned: (result: String) -> Unit,
|
onScanned: (result: String) -> Unit,
|
||||||
|
permissionState: PermissionState,
|
||||||
setScanState: (ScanState) -> Unit,
|
setScanState: (ScanState) -> Unit,
|
||||||
permissionState: PermissionState
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
// we check the permission first, as the ProcessCameraProvider's emit won't be called again after
|
// We check the permission first, as the ProcessCameraProvider's emit won't be called again after
|
||||||
// recomposition with the permission granted
|
// recomposition with the permission granted
|
||||||
val cameraProviderFlow =
|
val cameraProviderFlow =
|
||||||
if (permissionState.status.isGranted) {
|
if (permissionState.status.isGranted) {
|
||||||
|
@ -355,6 +517,9 @@ fun ScanCameraView(
|
||||||
|
|
||||||
val collectedCameraProvider = cameraProviderFlow?.collectAsState(initial = null)?.value
|
val collectedCameraProvider = cameraProviderFlow?.collectAsState(initial = null)?.value
|
||||||
|
|
||||||
|
val cameraController = remember { mutableStateOf<CameraControl?>(null) }
|
||||||
|
cameraController.value?.enableTorch(isTorchOn)
|
||||||
|
|
||||||
if (null == collectedCameraProvider) {
|
if (null == collectedCameraProvider) {
|
||||||
// Show loading indicator
|
// Show loading indicator
|
||||||
} else {
|
} else {
|
||||||
|
@ -387,14 +552,15 @@ fun ScanCameraView(
|
||||||
}
|
}
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
// we must unbind the use-cases before rebinding them
|
// We must unbind the use-cases before rebinding them
|
||||||
collectedCameraProvider.unbindAll()
|
collectedCameraProvider.unbindAll()
|
||||||
collectedCameraProvider.bindToLifecycle(
|
cameraController.value =
|
||||||
lifecycleOwner,
|
collectedCameraProvider.bindToLifecycle(
|
||||||
selector,
|
lifecycleOwner,
|
||||||
preview,
|
selector,
|
||||||
imageAnalysis
|
preview,
|
||||||
)
|
imageAnalysis
|
||||||
|
).cameraControl
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Twig.error { "Scan QR failed in bind phase with: ${it.message}" }
|
Twig.error { "Scan QR failed in bind phase with: ${it.message}" }
|
||||||
setScanState(ScanState.Failed)
|
setScanState(ScanState.Failed)
|
||||||
|
@ -402,12 +568,16 @@ fun ScanCameraView(
|
||||||
|
|
||||||
previewView
|
previewView
|
||||||
},
|
},
|
||||||
Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier
|
||||||
.testTag(ScanTag.CAMERA_VIEW)
|
.fillMaxSize()
|
||||||
|
.testTag(ScanTag.CAMERA_VIEW)
|
||||||
)
|
)
|
||||||
|
|
||||||
imageAnalysis.qrCodeFlow(context).collectAsState(initial = null).value?.let {
|
imageAnalysis.qrCodeFlow(
|
||||||
|
context = context,
|
||||||
|
framePosition = framePosition,
|
||||||
|
).collectAsState(initial = null).value?.let {
|
||||||
onScanned(it)
|
onScanned(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -416,17 +586,23 @@ fun ScanCameraView(
|
||||||
// Using callbackFlow because QrCodeAnalyzer has a non-suspending callback which makes
|
// Using callbackFlow because QrCodeAnalyzer has a non-suspending callback which makes
|
||||||
// a basic flow builder not work here.
|
// a basic flow builder not work here.
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageAnalysis.qrCodeFlow(context: Context): Flow<String> =
|
fun ImageAnalysis.qrCodeFlow(
|
||||||
|
context: Context,
|
||||||
|
framePosition: FramePosition,
|
||||||
|
): Flow<String> =
|
||||||
remember {
|
remember {
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
setAnalyzer(
|
setAnalyzer(
|
||||||
ContextCompat.getMainExecutor(context),
|
ContextCompat.getMainExecutor(context),
|
||||||
QrCodeAnalyzer { result ->
|
QrCodeAnalyzer(
|
||||||
// Note that these callbacks aren't tied to the Compose lifecycle, so they could occur
|
framePosition = framePosition,
|
||||||
// after the view goes away. Collection needs to occur within the Compose lifecycle
|
onQrCodeScanned = { result ->
|
||||||
// to make this not be a problem.
|
// Note that these callbacks aren't tied to the Compose lifecycle, so they could occur
|
||||||
trySend(result)
|
// after the view goes away. Collection needs to occur within the Compose lifecycle
|
||||||
}
|
// to make this not be a problem.
|
||||||
|
trySend(result)
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
@file:Suppress("ktlint:standard:filename")
|
|
||||||
|
|
||||||
package co.electriccoin.zcash.ui.screen.settings
|
package co.electriccoin.zcash.ui.screen.settings
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<string name="about_version_format" formatted="true">Version <xliff:g example="1.0" id="version_name">%1$s
|
<string name="about_version_format" formatted="true">Version <xliff:g example="1.0" id="version_name">%1$s
|
||||||
</xliff:g></string>
|
</xliff:g></string>
|
||||||
<string name="about_debug_menu_app_name">App name:
|
<string name="about_debug_menu_app_name">App name:
|
||||||
<xliff:g example="Zashi (D)" id="app_name">%1$s</xliff:g></string>
|
<xliff:g example="Zashi" id="app_name">%1$s</xliff:g></string>
|
||||||
<string name="about_debug_menu_build">Build: <xliff:g example="635dac0eb9ddc2bc6da5177f0dd495d8b76af4dc"
|
<string name="about_debug_menu_build">Build: <xliff:g example="635dac0eb9ddc2bc6da5177f0dd495d8b76af4dc"
|
||||||
id="git_commit_hash">%1$s</xliff:g></string>
|
id="git_commit_hash">%1$s</xliff:g></string>
|
||||||
<string name="about_description">Send and receive ZEC on Zashi! Zashi is a minimal-design, self-custody, ZEC-only
|
<string name="about_description">Send and receive ZEC on Zashi! Zashi is a minimal-design, self-custody, ZEC-only
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="37dp"
|
||||||
|
android:viewportWidth="36"
|
||||||
|
android:viewportHeight="37">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,1.75L36,1.75"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M1,37L1,1"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="18dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="18"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M3.167,0h11v20h-11z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M11.323,0C11.49,0.1 11.537,0.249 11.509,0.448C11.334,1.777 11.164,3.108 10.993,4.439C10.832,5.682 10.672,6.924 10.513,8.167C10.506,8.22 10.506,8.275 10.502,8.354C10.574,8.354 10.634,8.354 10.695,8.354C11.727,8.354 12.759,8.354 13.789,8.354C13.913,8.354 14.03,8.362 14.11,8.481C14.198,8.612 14.185,8.747 14.06,8.921C13.406,9.821 12.752,10.721 12.098,11.62C10.136,14.32 8.174,17.019 6.212,19.719C6.174,19.771 6.137,19.826 6.096,19.877C5.994,20.001 5.857,20.033 5.732,19.965C5.6,19.894 5.54,19.749 5.585,19.584C5.709,19.137 5.838,18.692 5.965,18.246C6.54,16.219 7.116,14.191 7.691,12.164C7.706,12.112 7.715,12.059 7.732,11.984C7.656,11.984 7.596,11.984 7.535,11.984C6.232,11.984 4.93,11.984 3.628,11.984C3.582,11.984 3.534,11.984 3.488,11.984C3.357,11.984 3.254,11.933 3.195,11.803C3.14,11.677 3.174,11.566 3.246,11.461C3.605,10.936 3.964,10.411 4.322,9.886C6.523,6.663 8.725,3.442 10.929,0.22C10.986,0.138 11.066,0.072 11.136,0C11.198,0 11.261,0 11.323,0Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</group>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M0.396,2.397L17.646,19.647"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="12dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="12"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0.667,0h11v20h-11z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8.823,0C8.99,0.1 9.037,0.249 9.009,0.448C8.834,1.777 8.664,3.108 8.493,4.439C8.332,5.682 8.172,6.924 8.013,8.167C8.006,8.22 8.006,8.275 8.002,8.354C8.074,8.354 8.134,8.354 8.195,8.354C9.227,8.354 10.259,8.354 11.289,8.354C11.413,8.354 11.53,8.362 11.61,8.481C11.698,8.612 11.685,8.747 11.56,8.921C10.906,9.821 10.252,10.721 9.598,11.62C7.636,14.32 5.674,17.019 3.712,19.719C3.674,19.771 3.637,19.826 3.596,19.877C3.494,20.001 3.357,20.033 3.232,19.965C3.1,19.894 3.04,19.749 3.085,19.584C3.209,19.137 3.338,18.692 3.465,18.246C4.04,16.219 4.616,14.191 5.191,12.164C5.206,12.112 5.215,12.059 5.232,11.984C5.156,11.984 5.096,11.984 5.035,11.984C3.732,11.984 2.43,11.984 1.128,11.984C1.082,11.984 1.034,11.984 0.988,11.984C0.857,11.984 0.754,11.933 0.695,11.803C0.64,11.677 0.674,11.566 0.746,11.461C1.105,10.936 1.464,10.411 1.822,9.886C4.023,6.663 6.225,3.442 8.429,0.22C8.486,0.138 8.566,0.072 8.636,0C8.698,0 8.761,0 8.823,0Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -1,20 +1,18 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<resources>
|
<string name="scan_back">Back</string>
|
||||||
<string name="scan_header">Scan a Zcash QR</string>
|
|
||||||
<string name="scan_back_content_description">Back</string>
|
<string name="scan_back_content_description">Back</string>
|
||||||
<string name="scan_preview_content_description">Camera</string>
|
<string name="scan_preview_content_description">Camera</string>
|
||||||
|
|
||||||
<string name="scan_hint">You can scan any Zcash QR code.</string>
|
<string name="scan_cancel_button">Cancel</string>
|
||||||
|
<string name="scan_settings_button">Open Settings</string>
|
||||||
<string name="scan_settings_button">Enable camera permission</string>
|
|
||||||
<string name="scan_settings_open_failed">Unable to launch Settings app.</string>
|
<string name="scan_settings_open_failed">Unable to launch Settings app.</string>
|
||||||
|
|
||||||
<string name="scan_setup_failed">Unable to run QR scanner.</string>
|
<string name="scan_state_permission">
|
||||||
<string name="scan_setup_back">Back</string>
|
<xliff:g id="app_name" example="Zashi">%1$s</xliff:g> does not have access to your camera. Please go to your
|
||||||
|
system settings and authorize <xliff:g id="app_name" example="Zashi">%1$s</xliff:g>.
|
||||||
|
</string>
|
||||||
|
<string name="scan_state_failed">Camera initialization failed. Try it again, please.</string>
|
||||||
|
<string name="scan_address_validation_failed">This QR code is not a valid Zcash Address.</string>
|
||||||
|
|
||||||
<string name="scan_state_permission">Permission for camera is necessary.</string>
|
<string name="scan_torch_content_description">Camera torch toggle</string>
|
||||||
<string name="scan_state_scanning">Scanning…</string>
|
|
||||||
<string name="scan_state_failed">Scanning failed.</string>
|
|
||||||
|
|
||||||
<string name="scan_validation_invalid_address">Invalid Zcash address scanned.</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue