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) { while (isActive) {
val result = wallet.awaitRefresh() val result = wallet.awaitRefresh()
if (result.isError()) { if (result.isError()) {
break; break
} }
delay(10.seconds) delay(10.seconds)
} }

View File

@ -48,7 +48,7 @@ private fun FirstStepScreen(
Scaffold( Scaffold(
topBar = { topBar = {
Toolbar( Toolbar(
titleRes = R.string.add_wallet, title = stringResource(R.string.add_wallet),
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
Icon( Icon(
@ -125,9 +125,9 @@ private fun SecondStepScreen(
) { ) {
Scaffold( Scaffold(
topBar = { 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( Toolbar(
titleRes = title, title = stringResource(titleRes),
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
Icon( Icon(

View File

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

View File

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

View File

@ -1,13 +1,14 @@
package im.molly.monero.demo.ui package im.molly.monero.demo.ui
import android.net.Uri
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -16,6 +17,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.demo.R import im.molly.monero.demo.R
import im.molly.monero.demo.data.model.RemoteNode import im.molly.monero.demo.data.model.RemoteNode
import im.molly.monero.demo.ui.theme.AppIcons
import im.molly.monero.demo.ui.theme.AppTheme import im.molly.monero.demo.ui.theme.AppTheme
@Composable @Composable
@ -46,19 +48,46 @@ private fun SettingsScreen(
Column( Column(
modifier = modifier modifier = modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(all = 24.dp)
) { ) {
Divider() SettingsSection(
header = {
SettingsSectionTitle(R.string.remote_nodes) SettingsSectionTitle(R.string.remote_nodes)
TextButton(onClick = onAddRemoteNode) { IconButton(onClick = onAddRemoteNode) {
Text(stringResource(R.string.add_remote_node)) Icon(
imageVector = AppIcons.AddRemoteWallet,
contentDescription = stringResource(R.string.add_remote_node),
)
} }
}
)
RemoteNodeEditableList( RemoteNodeEditableList(
remoteNodes = remoteNodes, remoteNodes = remoteNodes,
onEditRemoteNode = onEditRemoteNode, onEditRemoteNode = onEditRemoteNode,
onDeleteRemoteNode = onDeleteRemoteNode, 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(
text = stringResource(titleRes), text = stringResource(titleRes),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
) )
} }
@ -75,8 +103,9 @@ private fun SettingsSectionTitle(@StringRes titleRes: Int) {
@Composable @Composable
private fun SettingsScreenPreview() { private fun SettingsScreenPreview() {
AppTheme { AppTheme {
val aNode = RemoteNode.EMPTY.copy(uri = Uri.parse("http://node.monero"))
SettingsScreen( 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.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -12,10 +13,12 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun WalletCard( fun WalletCard(
walletId: Long, walletId: Long,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: (Long) -> Unit,
viewModel: WalletViewModel = viewModel( viewModel: WalletViewModel = viewModel(
factory = WalletViewModel.factory(walletId), factory = WalletViewModel.factory(walletId),
key = walletId.toString(), key = walletId.toString(),
@ -24,6 +27,7 @@ fun WalletCard(
val walletUiState: WalletUiState by viewModel.walletUiState.collectAsStateWithLifecycle() val walletUiState: WalletUiState by viewModel.walletUiState.collectAsStateWithLifecycle()
Card( Card(
onClick = { onClick(walletId) },
modifier = modifier modifier = modifier
.padding(8.dp) .padding(8.dp)
) { ) {

View File

@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
fun LazyListScope.walletCardsItems( fun LazyListScope.walletCardsItems(
items: List<Long>, items: List<Long>,
onItemClick: (Long) -> Unit,
itemModifier: Modifier = Modifier, itemModifier: Modifier = Modifier,
) = items( ) = items(
items = items, items = items,
@ -13,6 +14,7 @@ fun LazyListScope.walletCardsItems(
itemContent = { itemContent = {
WalletCard( WalletCard(
walletId = it, walletId = it,
onClick = onItemClick,
modifier = itemModifier, 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 package im.molly.monero.demo.ui.component
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -8,19 +7,18 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun Toolbar( fun Toolbar(
@StringRes titleRes: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String? = null,
navigationIcon: @Composable () -> Unit = {}, navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null, scrollBehavior: TopAppBarScrollBehavior? = null,
) { ) {
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { Text(stringResource(id = titleRes), ) }, title = { Text(title ?: "") },
modifier = modifier, modifier = modifier,
navigationIcon = navigationIcon, navigationIcon = navigationIcon,
actions = actions, actions = actions,

View File

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

View File

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

View File

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

View File

@ -4,13 +4,22 @@ import androidx.navigation.*
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import im.molly.monero.demo.ui.AddWalletFirstStepRoute import im.molly.monero.demo.ui.AddWalletFirstStepRoute
import im.molly.monero.demo.ui.AddWalletSecondStepRoute 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 startNavRoute = "$addWalletWizardNavRoute/start"
private const val createNavRoute = "$addWalletWizardNavRoute/create" private const val createNavRoute = "$addWalletWizardNavRoute/create"
private const val restoreNavRoute = "$addWalletWizardNavRoute/restore" private const val restoreNavRoute = "$addWalletWizardNavRoute/restore"
fun NavController.navigateToWallet(walletId: Long) {
val route = "$walletNavRoute/$walletId"
navigate(route)
}
fun NavController.navigateToAddWalletWizardGraph(navOptions: NavOptions? = null) { fun NavController.navigateToAddWalletWizardGraph(navOptions: NavOptions? = null) {
navigate(addWalletWizardNavRoute, navOptions) 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( fun NavGraphBuilder.addWalletWizardGraph(
navController: NavHostController, navController: NavHostController,
onBackClick: () -> Unit, 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.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.*
import androidx.compose.material.icons.outlined.List
import androidx.compose.material.icons.outlined.Settings
object AppIcons { object AppIcons {
val ArrowBack = Icons.Filled.ArrowBack val ArrowBack = Icons.Filled.ArrowBack
@ -16,4 +14,5 @@ object AppIcons {
val Settings = Icons.Filled.Settings val Settings = Icons.Filled.Settings
val SettingsOutlined = Icons.Outlined.Settings val SettingsOutlined = Icons.Outlined.Settings
val AddWallet = Icons.Filled.Add val AddWallet = Icons.Filled.Add
val AddRemoteWallet = Icons.Outlined.AddCircle
} }

View File

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