441 lines
16 KiB
Kotlin
441 lines
16 KiB
Kotlin
@file:Suppress("TooManyFunctions")
|
|
|
|
package co.electriccoin.zcash.ui.screen.home.view
|
|
|
|
import android.content.res.Configuration
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.aspectRatio
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.wrapContentSize
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.ContactSupport
|
|
import androidx.compose.material.icons.filled.Info
|
|
import androidx.compose.material.icons.filled.Menu
|
|
import androidx.compose.material.icons.filled.MoreVert
|
|
import androidx.compose.material.icons.filled.Password
|
|
import androidx.compose.material.icons.filled.Settings
|
|
import androidx.compose.material3.CircularProgressIndicator
|
|
import androidx.compose.material3.DrawerState
|
|
import androidx.compose.material3.DrawerValue
|
|
import androidx.compose.material3.DropdownMenu
|
|
import androidx.compose.material3.DropdownMenuItem
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.IconButton
|
|
import androidx.compose.material3.ModalDrawerSheet
|
|
import androidx.compose.material3.ModalNavigationDrawer
|
|
import androidx.compose.material3.NavigationDrawerItem
|
|
import androidx.compose.material3.NavigationDrawerItemDefaults
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TopAppBar
|
|
import androidx.compose.material3.rememberDrawerState
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.platform.LocalConfiguration
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.platform.testTag
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.tooling.preview.Preview
|
|
import androidx.compose.ui.unit.dp
|
|
import cash.z.ecc.android.sdk.Synchronizer
|
|
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
|
|
import cash.z.ecc.android.sdk.model.PercentDecimal
|
|
import co.electriccoin.zcash.crash.android.GlobalCrashReporter
|
|
import co.electriccoin.zcash.ui.R
|
|
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
|
|
import co.electriccoin.zcash.ui.common.closeDrawerMenu
|
|
import co.electriccoin.zcash.ui.common.openDrawerMenu
|
|
import co.electriccoin.zcash.ui.design.component.Body
|
|
import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol
|
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
|
import co.electriccoin.zcash.ui.design.component.HeaderWithZecIcon
|
|
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
|
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
|
import co.electriccoin.zcash.ui.screen.home.HomeTag
|
|
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
|
|
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues
|
|
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
|
import kotlinx.collections.immutable.ImmutableList
|
|
import kotlinx.collections.immutable.persistentListOf
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
@Preview
|
|
@Composable
|
|
fun ComposablePreview() {
|
|
ZcashTheme(darkTheme = true) {
|
|
GradientSurface {
|
|
Home(
|
|
walletSnapshot = WalletSnapshotFixture.new(),
|
|
transactionHistory = persistentListOf(),
|
|
isUpdateAvailable = false,
|
|
isKeepScreenOnDuringSync = false,
|
|
isDebugMenuEnabled = false,
|
|
isFiatConversionEnabled = false,
|
|
goSeedPhrase = {},
|
|
goSettings = {},
|
|
goSupport = {},
|
|
goAbout = {},
|
|
goReceive = {},
|
|
goSend = {},
|
|
resetSdk = {}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Suppress("LongParameterList")
|
|
@Composable
|
|
fun Home(
|
|
walletSnapshot: WalletSnapshot,
|
|
transactionHistory: ImmutableList<CommonTransaction>,
|
|
isUpdateAvailable: Boolean,
|
|
isKeepScreenOnDuringSync: Boolean?,
|
|
isFiatConversionEnabled: Boolean,
|
|
isDebugMenuEnabled: Boolean,
|
|
goSeedPhrase: () -> Unit,
|
|
goSettings: () -> Unit,
|
|
goSupport: () -> Unit,
|
|
goAbout: () -> Unit,
|
|
goReceive: () -> Unit,
|
|
goSend: () -> Unit,
|
|
resetSdk: () -> Unit,
|
|
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
|
|
scope: CoroutineScope = rememberCoroutineScope()
|
|
) {
|
|
ModalNavigationDrawer(
|
|
drawerState = drawerState,
|
|
drawerContent = {
|
|
HomeDrawer(
|
|
onCloseDrawer = { drawerState.closeDrawerMenu(scope) },
|
|
goSeedPhrase = goSeedPhrase,
|
|
goSettings = goSettings,
|
|
goSupport = goSupport,
|
|
goAbout = goAbout
|
|
)
|
|
},
|
|
content = {
|
|
Scaffold(topBar = {
|
|
HomeTopAppBar(
|
|
isDebugMenuEnabled = isDebugMenuEnabled,
|
|
openDrawer = { drawerState.openDrawerMenu(scope) },
|
|
resetSdk = resetSdk
|
|
)
|
|
}) { paddingValues ->
|
|
HomeMainContent(
|
|
paddingValues,
|
|
walletSnapshot,
|
|
transactionHistory,
|
|
isUpdateAvailable = isUpdateAvailable,
|
|
isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
|
|
isFiatConversionEnabled = isFiatConversionEnabled,
|
|
goReceive = goReceive,
|
|
goSend = goSend,
|
|
)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
private fun HomeTopAppBar(
|
|
isDebugMenuEnabled: Boolean,
|
|
openDrawer: () -> Unit,
|
|
resetSdk: () -> Unit,
|
|
) {
|
|
TopAppBar(
|
|
title = { Text(text = stringResource(id = R.string.app_name)) },
|
|
navigationIcon = {
|
|
IconButton(
|
|
onClick = openDrawer
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Filled.Menu,
|
|
contentDescription = stringResource(R.string.home_menu_content_description)
|
|
)
|
|
}
|
|
},
|
|
actions = {
|
|
if (isDebugMenuEnabled) {
|
|
DebugMenu(resetSdk)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun DebugMenu(
|
|
resetSdk: () -> Unit
|
|
) {
|
|
Column {
|
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
|
IconButton(onClick = { expanded = true }) {
|
|
Icon(Icons.Default.MoreVert, contentDescription = null)
|
|
}
|
|
|
|
DropdownMenu(
|
|
expanded = expanded,
|
|
onDismissRequest = { expanded = false }
|
|
) {
|
|
DropdownMenuItem(
|
|
text = { Text("Throw Uncaught Exception") },
|
|
onClick = {
|
|
// Supposed to be generic, for manual debugging only
|
|
@Suppress("TooGenericExceptionThrown")
|
|
throw RuntimeException("Manually crashed from debug menu")
|
|
}
|
|
)
|
|
DropdownMenuItem(
|
|
text = { Text("Report Caught Exception") },
|
|
onClick = {
|
|
// Eventually this shouldn't rely on the Android implementation, but rather an expect/actual
|
|
// should be used at the crash API level.
|
|
GlobalCrashReporter.reportCaughtException(RuntimeException("Manually caught exception from debug menu"))
|
|
expanded = false
|
|
}
|
|
)
|
|
DropdownMenuItem(
|
|
text = { Text("Reset SDK") },
|
|
onClick = {
|
|
resetSdk()
|
|
expanded = false
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun HomeDrawer(
|
|
onCloseDrawer: () -> Unit,
|
|
goSeedPhrase: () -> Unit,
|
|
goSettings: () -> Unit,
|
|
goSupport: () -> Unit,
|
|
goAbout: () -> Unit,
|
|
) {
|
|
ModalDrawerSheet {
|
|
Spacer(Modifier.height(12.dp))
|
|
NavigationDrawerItem(
|
|
icon = { Icon(Icons.Default.Password, contentDescription = null) },
|
|
label = { Text(stringResource(id = R.string.home_menu_seed_phrase)) },
|
|
selected = false,
|
|
onClick = {
|
|
onCloseDrawer()
|
|
goSeedPhrase()
|
|
},
|
|
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
|
)
|
|
NavigationDrawerItem(
|
|
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
|
|
label = { Text(stringResource(id = R.string.home_menu_settings)) },
|
|
selected = false,
|
|
onClick = {
|
|
onCloseDrawer()
|
|
goSettings()
|
|
},
|
|
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
|
)
|
|
NavigationDrawerItem(
|
|
icon = { Icon(Icons.Default.ContactSupport, contentDescription = null) },
|
|
label = { Text(stringResource(id = R.string.home_menu_support)) },
|
|
selected = false,
|
|
onClick = {
|
|
onCloseDrawer()
|
|
goSupport()
|
|
},
|
|
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
|
)
|
|
NavigationDrawerItem(
|
|
icon = { Icon(Icons.Default.Info, contentDescription = null) },
|
|
label = { Text(stringResource(id = R.string.home_menu_about)) },
|
|
selected = false,
|
|
onClick = {
|
|
onCloseDrawer()
|
|
goAbout()
|
|
},
|
|
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Suppress("LongParameterList")
|
|
@Composable
|
|
private fun HomeMainContent(
|
|
paddingValues: PaddingValues,
|
|
walletSnapshot: WalletSnapshot,
|
|
transactionHistory: ImmutableList<CommonTransaction>,
|
|
isUpdateAvailable: Boolean,
|
|
isKeepScreenOnDuringSync: Boolean?,
|
|
isFiatConversionEnabled: Boolean,
|
|
goReceive: () -> Unit,
|
|
goSend: () -> Unit,
|
|
) {
|
|
Column(
|
|
Modifier
|
|
.verticalScroll(rememberScrollState())
|
|
.padding(top = paddingValues.calculateTopPadding())
|
|
) {
|
|
Status(walletSnapshot, isUpdateAvailable, isFiatConversionEnabled)
|
|
|
|
Spacer(modifier = Modifier.height(24.dp))
|
|
|
|
PrimaryButton(onClick = goReceive, text = stringResource(R.string.home_button_receive))
|
|
PrimaryButton(onClick = goSend, text = stringResource(R.string.home_button_send))
|
|
|
|
History(transactionHistory)
|
|
|
|
if (isKeepScreenOnDuringSync == true && isSyncing(walletSnapshot.status)) {
|
|
DisableScreenTimeout()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun isSyncing(status: Synchronizer.Status): Boolean {
|
|
return status == Synchronizer.Status.DOWNLOADING ||
|
|
status == Synchronizer.Status.VALIDATING ||
|
|
status == Synchronizer.Status.SCANNING ||
|
|
status == Synchronizer.Status.ENHANCING
|
|
}
|
|
|
|
@Composable
|
|
@Suppress("LongMethod", "MagicNumber")
|
|
private fun Status(
|
|
walletSnapshot: WalletSnapshot,
|
|
updateAvailable: Boolean,
|
|
isFiatConversionEnabled: Boolean
|
|
) {
|
|
val configuration = LocalConfiguration.current
|
|
val contentSizeRatioRatio = if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
0.45f
|
|
} else {
|
|
0.9f
|
|
}
|
|
|
|
// UI parts sizes
|
|
val progressCircleStroke = 12.dp
|
|
val progressCirclePadding = progressCircleStroke + 6.dp
|
|
val contentPadding = progressCircleStroke + progressCirclePadding + 10.dp
|
|
|
|
val walletDisplayValues = WalletDisplayValues.getNextValues(
|
|
LocalContext.current,
|
|
walletSnapshot,
|
|
updateAvailable
|
|
)
|
|
|
|
// wrapper box
|
|
Box(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.testTag(HomeTag.STATUS_VIEWS),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
// relatively sized box
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxWidth(contentSizeRatioRatio)
|
|
.aspectRatio(1f),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
// progress circle
|
|
if (walletDisplayValues.progress.decimal > PercentDecimal.ZERO_PERCENT.decimal) {
|
|
CircularProgressIndicator(
|
|
progress = walletDisplayValues.progress.decimal,
|
|
color = Color.Gray,
|
|
strokeWidth = progressCircleStroke,
|
|
modifier = Modifier
|
|
.matchParentSize()
|
|
.padding(progressCirclePadding)
|
|
.testTag(HomeTag.PROGRESS)
|
|
)
|
|
}
|
|
|
|
// texts
|
|
Column(
|
|
modifier = Modifier
|
|
.padding(contentPadding)
|
|
.wrapContentSize(),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
Spacer(modifier = Modifier.height(24.dp))
|
|
|
|
if (walletDisplayValues.zecAmountText.isNotEmpty()) {
|
|
HeaderWithZecIcon(amount = walletDisplayValues.zecAmountText)
|
|
}
|
|
|
|
if (isFiatConversionEnabled) {
|
|
Column(Modifier.testTag(HomeTag.FIAT_CONVERSION)) {
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
when (walletDisplayValues.fiatCurrencyAmountState) {
|
|
is FiatCurrencyConversionRateState.Current -> {
|
|
BodyWithFiatCurrencySymbol(
|
|
amount = walletDisplayValues.fiatCurrencyAmountText
|
|
)
|
|
}
|
|
is FiatCurrencyConversionRateState.Stale -> {
|
|
// Note: we should show information about staleness too
|
|
BodyWithFiatCurrencySymbol(
|
|
amount = walletDisplayValues.fiatCurrencyAmountText
|
|
)
|
|
}
|
|
is FiatCurrencyConversionRateState.Unavailable -> {
|
|
Body(text = walletDisplayValues.fiatCurrencyAmountText)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(24.dp))
|
|
|
|
if (walletDisplayValues.statusText.isNotEmpty()) {
|
|
Body(
|
|
text = walletDisplayValues.statusText,
|
|
modifier = Modifier.testTag(HomeTag.SINGLE_LINE_TEXT)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
@Suppress("MagicNumber")
|
|
private fun History(transactionHistory: ImmutableList<CommonTransaction>) {
|
|
if (transactionHistory.isEmpty()) {
|
|
return
|
|
}
|
|
|
|
// here we need to use a fixed height to avoid nested columns vertical scrolling problem
|
|
// we'll refactor this part to a dedicated bottom sheet later
|
|
val historyPart = LocalConfiguration.current.screenHeightDp / 3
|
|
|
|
LazyColumn(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.height(historyPart.dp)
|
|
) {
|
|
items(transactionHistory) {
|
|
Text(it.toString())
|
|
}
|
|
}
|
|
}
|