demo: add wallet screen

This commit is contained in:
Oscar Mira 2023-03-29 10:11:27 +02:00
parent 518e9234fe
commit 41050d28ed
15 changed files with 210 additions and 55 deletions

View File

@ -42,7 +42,7 @@ class SyncService(
while (isActive) {
val result = wallet.awaitRefresh()
if (result.isError()) {
break;
break
}
delay(10.seconds)
}

View File

@ -48,7 +48,7 @@ private fun FirstStepScreen(
Scaffold(
topBar = {
Toolbar(
titleRes = R.string.add_wallet,
title = stringResource(R.string.add_wallet),
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
@ -125,9 +125,9 @@ private fun SecondStepScreen(
) {
Scaffold(
topBar = {
val title = if (showRestoreOptions) R.string.restore_wallet else R.string.new_wallet
val titleRes = if (showRestoreOptions) R.string.restore_wallet else R.string.new_wallet
Toolbar(
titleRes = title,
title = stringResource(titleRes),
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(

View File

@ -19,6 +19,7 @@ import im.molly.monero.demo.ui.component.Toolbar
@Composable
fun HomeRoute(
navigateToAddWalletWizard: () -> Unit,
navigateToWallet: (Long) -> Unit,
viewModel: HomeViewModel = viewModel(),
) {
val walletListUiState: WalletListUiState by viewModel.walletListUiState.collectAsStateWithLifecycle()
@ -26,6 +27,7 @@ fun HomeRoute(
HomeScreen(
walletListUiState = walletListUiState,
onAddWalletClick = navigateToAddWalletWizard,
onWalletClick = navigateToWallet,
)
}
@ -34,13 +36,14 @@ fun HomeRoute(
private fun HomeScreen(
walletListUiState: WalletListUiState,
onAddWalletClick: () -> Unit,
onWalletClick: (Long) -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Toolbar(
titleRes = R.string.monero_wallets,
title = stringResource(R.string.monero_wallets),
scrollBehavior = scrollBehavior,
)
},
@ -57,20 +60,21 @@ private fun HomeScreen(
.fillMaxSize()
.padding(padding))
{
walletCards(walletListUiState)
walletCards(walletListUiState, onWalletClick)
}
}
}
private fun LazyListScope.walletCards(
walletListUiState: WalletListUiState,
onWalletClick: (Long) -> Unit,
) {
when (walletListUiState) {
WalletListUiState.Loading -> item {
Text(text = "Loading wallet list...") // TODO
}
is WalletListUiState.Success -> {
walletCardsItems(walletListUiState.ids)
walletCardsItems(walletListUiState.ids, onWalletClick)
}
}
}

View File

@ -1,8 +1,7 @@
package im.molly.monero.demo.ui
import android.net.Uri
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@ -41,9 +40,7 @@ fun RemoteNodeEditableList(
onDeleteRemoteNode: (RemoteNode) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
) {
Column(modifier = modifier) {
remoteNodes.forEach { remoteNode ->
RemoteNodeItem(
remoteNode,
@ -70,7 +67,6 @@ private fun RemoteNodeItem(
headlineText = { Text(remoteNode.uri.toString()) },
overlineText = { Text(remoteNode.network.name.uppercase()) },
trailingContent = {
Row {
if (showCheckbox) {
Checkbox(
checked = checked,
@ -78,18 +74,17 @@ private fun RemoteNodeItem(
)
}
if (showMenu) {
KebabMenu(
WalletKebabMenu(
onEditClick = { onEditClick(remoteNode) },
onDeleteClick = { onDeleteClick(remoteNode) },
)
}
}
},
)
}
@Composable
private fun KebabMenu(
private fun WalletKebabMenu(
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
) {

View File

@ -1,13 +1,14 @@
package im.molly.monero.demo.ui
import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -16,6 +17,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.demo.R
import im.molly.monero.demo.data.model.RemoteNode
import im.molly.monero.demo.ui.theme.AppIcons
import im.molly.monero.demo.ui.theme.AppTheme
@Composable
@ -46,19 +48,46 @@ private fun SettingsScreen(
Column(
modifier = modifier
.verticalScroll(rememberScrollState())
.padding(all = 24.dp)
) {
Divider()
SettingsSection(
header = {
SettingsSectionTitle(R.string.remote_nodes)
TextButton(onClick = onAddRemoteNode) {
Text(stringResource(R.string.add_remote_node))
IconButton(onClick = onAddRemoteNode) {
Icon(
imageVector = AppIcons.AddRemoteWallet,
contentDescription = stringResource(R.string.add_remote_node),
)
}
}
)
RemoteNodeEditableList(
remoteNodes = remoteNodes,
onEditRemoteNode = onEditRemoteNode,
onDeleteRemoteNode = onDeleteRemoteNode,
modifier = Modifier.padding(start = 24.dp),
)
}
}
@Composable
private fun SettingsSection(
modifier: Modifier = Modifier,
header: @Composable() (RowScope.() -> Unit),
content: @Composable() (RowScope.() -> Unit) = {},
) {
Column(
modifier = modifier.padding(horizontal = 24.dp),
) {
Divider(Modifier.padding(top = 24.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.fillMaxWidth(),
content = header,
)
Row(
content = content,
)
// Divider()
}
}
@ -67,7 +96,6 @@ private fun SettingsSectionTitle(@StringRes titleRes: Int) {
Text(
text = stringResource(titleRes),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
)
}
@ -75,8 +103,9 @@ private fun SettingsSectionTitle(@StringRes titleRes: Int) {
@Composable
private fun SettingsScreenPreview() {
AppTheme {
val aNode = RemoteNode.EMPTY.copy(uri = Uri.parse("http://node.monero"))
SettingsScreen(
remoteNodes = emptyList(),
remoteNodes = listOf(aNode),
)
}
}

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -12,10 +13,12 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WalletCard(
walletId: Long,
modifier: Modifier = Modifier,
onClick: (Long) -> Unit,
viewModel: WalletViewModel = viewModel(
factory = WalletViewModel.factory(walletId),
key = walletId.toString(),
@ -24,6 +27,7 @@ fun WalletCard(
val walletUiState: WalletUiState by viewModel.walletUiState.collectAsStateWithLifecycle()
Card(
onClick = { onClick(walletId) },
modifier = modifier
.padding(8.dp)
) {

View File

@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
fun LazyListScope.walletCardsItems(
items: List<Long>,
onItemClick: (Long) -> Unit,
itemModifier: Modifier = Modifier,
) = items(
items = items,
@ -13,6 +14,7 @@ fun LazyListScope.walletCardsItems(
itemContent = {
WalletCard(
walletId = it,
onClick = onItemClick,
modifier = itemModifier,
)
},

View File

@ -0,0 +1,91 @@
package im.molly.monero.demo.ui
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.demo.R
import im.molly.monero.demo.ui.component.Toolbar
import im.molly.monero.demo.ui.theme.AppIcons
@Composable
fun WalletRoute(
walletId: Long,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
viewModel: WalletViewModel = viewModel(
factory = WalletViewModel.factory(walletId),
key = walletId.toString(),
)
) {
val walletUiState: WalletUiState by viewModel.walletUiState.collectAsStateWithLifecycle()
WalletScreen(
walletUiState = walletUiState,
onBackClick = onBackClick,
modifier = modifier,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun WalletScreen(
walletUiState: WalletUiState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
topBar = {
Toolbar(
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
},
actions = {
WalletKebabMenu({}, {})
}
)
}
) { padding ->
}
}
@Composable
private fun WalletKebabMenu(
onRenameClick: () -> Unit,
onDeleteClick: () -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = AppIcons.MoreVert,
contentDescription = stringResource(R.string.open_menu),
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.rename)) },
onClick = {
onRenameClick()
expanded = false
},
)
DropdownMenuItem(
text = { Text(stringResource(R.string.delete)) },
onClick = {
onDeleteClick()
expanded = false
},
)
}
}

View File

@ -1,6 +1,5 @@
package im.molly.monero.demo.ui.component
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
@ -8,19 +7,18 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Toolbar(
@StringRes titleRes: Int,
modifier: Modifier = Modifier,
title: String? = null,
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
CenterAlignedTopAppBar(
title = { Text(stringResource(id = titleRes), ) },
title = { Text(title ?: "") },
modifier = modifier,
navigationIcon = navigationIcon,
actions = actions,

View File

@ -14,10 +14,12 @@ fun NavController.navigateToHome(navOptions: NavOptions? = null) {
fun NavGraphBuilder.homeScreen(
navigateToAddWalletWizard: () -> Unit,
navigateToWallet: (Long) -> Unit,
) {
composable(route = homeNavRoute) {
HomeRoute(
navigateToAddWalletWizard = navigateToAddWalletWizard,
navigateToWallet = navigateToWallet,
)
}
}

View File

@ -18,6 +18,9 @@ fun NavGraph(
modifier,
) {
homeScreen(
navigateToWallet = { walletId ->
navController.navigateToWallet(walletId)
},
navigateToAddWalletWizard = {
navController.navigateToAddWalletWizardGraph()
},
@ -28,6 +31,9 @@ fun NavGraph(
navController.navigateToEditRemoteNode(remoteNodeId)
},
)
walletScreen(
onBackClick = onBackClick,
)
editRemoteNodeDialog(
onBackClick = onBackClick,
)

View File

@ -9,19 +9,16 @@ import im.molly.monero.demo.ui.SettingsRoute
const val settingsNavRoute = "settings"
const val settingsRemoteNodeNavRoute = "$settingsNavRoute/remote_node"
private const val idArg = "id"
private const val queryId = "id"
fun NavController.navigateToSettings(navOptions: NavOptions? = null) {
navigate(settingsNavRoute, navOptions)
}
fun NavController.navigateToEditRemoteNode(remoteNodeId: Long?) {
val route = if (remoteNodeId != null) {
"$settingsRemoteNodeNavRoute?$idArg=$remoteNodeId"
} else {
settingsRemoteNodeNavRoute
}
this.navigate(route)
val route = settingsRemoteNodeNavRoute +
if (remoteNodeId != null) "?$queryId=$remoteNodeId" else ""
navigate(route)
}
fun NavGraphBuilder.settingsScreen(
@ -38,16 +35,16 @@ fun NavGraphBuilder.editRemoteNodeDialog(
onBackClick: () -> Unit,
) {
dialog(
route = "$settingsRemoteNodeNavRoute?$idArg={$idArg}",
route = "$settingsRemoteNodeNavRoute?$queryId={$queryId}",
arguments = listOf(
navArgument(idArg) {
navArgument(queryId) {
type = NavType.StringType
nullable = true
}
)
) {
val arguments = requireNotNull(it.arguments)
val remoteNodeId = arguments.getString(idArg)?.toLongOrNull()
val remoteNodeId = arguments.getString(queryId)?.toLongOrNull()
EditRemoteNodeRoute(
remoteNodeId = remoteNodeId,
onBackClick = onBackClick,

View File

@ -4,13 +4,22 @@ import androidx.navigation.*
import androidx.navigation.compose.composable
import im.molly.monero.demo.ui.AddWalletFirstStepRoute
import im.molly.monero.demo.ui.AddWalletSecondStepRoute
import im.molly.monero.demo.ui.WalletRoute
const val addWalletWizardNavRoute = "home/add_wallet_wizard"
const val walletNavRoute = "wallet"
const val addWalletWizardNavRoute = "add_wallet_wizard"
private const val walletIdArg = "id"
private const val startNavRoute = "$addWalletWizardNavRoute/start"
private const val createNavRoute = "$addWalletWizardNavRoute/create"
private const val restoreNavRoute = "$addWalletWizardNavRoute/restore"
fun NavController.navigateToWallet(walletId: Long) {
val route = "$walletNavRoute/$walletId"
navigate(route)
}
fun NavController.navigateToAddWalletWizardGraph(navOptions: NavOptions? = null) {
navigate(addWalletWizardNavRoute, navOptions)
}
@ -22,6 +31,24 @@ fun NavController.navigateToAddWalletSecondStep(restoreWallet: Boolean) {
}
}
fun NavGraphBuilder.walletScreen(
onBackClick: () -> Unit,
) {
composable(
route = "$walletNavRoute/{$walletIdArg}",
arguments = listOf(
navArgument(walletIdArg) { type = NavType.LongType }
)
) {
val arguments = requireNotNull(it.arguments)
val walletId = arguments.getLong(walletIdArg)
WalletRoute(
walletId = walletId,
onBackClick = onBackClick,
)
}
}
fun NavGraphBuilder.addWalletWizardGraph(
navController: NavHostController,
onBackClick: () -> Unit,

View File

@ -2,9 +2,7 @@ package im.molly.monero.demo.ui.theme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.List
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.*
object AppIcons {
val ArrowBack = Icons.Filled.ArrowBack
@ -16,4 +14,5 @@ object AppIcons {
val Settings = Icons.Filled.Settings
val SettingsOutlined = Icons.Outlined.Settings
val AddWallet = Icons.Filled.Add
val AddRemoteWallet = Icons.Outlined.AddCircle
}

View File

@ -12,6 +12,7 @@
<string name="settings">Settings</string>
<string name="add_remote_node">Add remote node</string>
<string name="edit">Edit</string>
<string name="rename">Rename</string>
<string name="open_menu">Open menu</string>
<string name="delete">Delete</string>
<string name="save">Save</string>