Cw 426 replace trash and swipe with edit icons (#974)

* feat: Replace trash and swipe with edit icons on node list item

- replaces yellow Test button with red Delete node button with confirmation on the edit node page

* feat: make node indicator icons bigger (figma comment)

* feat: Replace trash and swipe with edit icons on wallet list page and create wallet_edit_page.dart

* fix: make delete buttons red

* fix: make wallet name wrap when it is too long

* refactor: improve logic & fix observer not refreshing

* fix: add string

* feat: remove the confirmation pop-up for switching between wallets

- which was another item on the jira issue

* fix: remove slideable widgets from node list

* feat: add edit button to currently selected node & disable deleting if selected

* fix: rename wallet also renames to new wallet files

* feat: make sure edits can't overlap existing names

* fix: improve rename flow, fix electrum transactions refresh & add delete old logic

* fix: also fix rename for monero & haven

* refactor: fix identations

* refactor: dont declare the current wallet twice

* refactor: missing newWalletInfo.id

* fix: dont unnecessarily load the current wallet

* fix: remove unnecessary reaction

* feat: make save button disabled until the text is changed

* feat: make walletEditViewModel and make state useful for pending actions

* fix: add back reaction for desktop flow

* - Remove un-necessary code
- Format Edit page

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Rafael Saes
2023-07-13 02:20:11 +03:00
committed by GitHub
co-authored by OmarHatem
parent bb6dea0292
commit d4969633b0
48 changed files with 670 additions and 249 deletions
@@ -1,6 +1,8 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/node.dart';
import 'package:flutter/material.dart';
@@ -122,17 +124,35 @@ class NodeCreateOrEditPage extends BasePage {
padding: EdgeInsets.only(right: 8.0),
child: LoadingPrimaryButton(
onPressed: () async {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
final confirmed = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle:
S.of(context).remove_node,
alertContent: S
.of(context)
.remove_node_message,
rightButtonText:
S.of(context).remove,
leftButtonText:
S.of(context).cancel,
actionRightButton: () =>
Navigator.pop(context, true),
actionLeftButton: () =>
Navigator.pop(context, false));
}) ??
false;
await nodeCreateOrEditViewModel.connect();
if (confirmed) {
await editingNode!.delete();
Navigator.of(context).pop();
}
},
isLoading: nodeCreateOrEditViewModel
.connectionState is IsExecutingState,
text: S.of(context).node_test,
isDisabled: !nodeCreateOrEditViewModel.isReady,
color: Colors.orange,
text: S.of(context).delete,
isDisabled: !nodeCreateOrEditViewModel.isReady ||
(isSelected ?? false),
color: Palette.red,
textColor: Colors.white),
)),
Flexible(
@@ -9,8 +9,8 @@ class NodeIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 8.0,
height: 8.0,
width: 12.0,
height: 12.0,
decoration: BoxDecoration(
shape: BoxShape.circle, color: isLive ? Palette.green : Palette.red),
);
@@ -1,21 +1,23 @@
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_indicator.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cw_core/node.dart';
import 'package:flutter/material.dart';
class NodeListRow extends StandardListRow {
NodeListRow(
{required String title,
required this.node,
required void Function(BuildContext context) onTap,
required bool isSelected,
required this.isAlive})
required bool isSelected})
: super(title: title, onTap: onTap, isSelected: isSelected);
final Future<bool> isAlive;
final Node node;
@override
Widget buildTrailing(BuildContext context) {
Widget buildLeading(BuildContext context) {
return FutureBuilder(
future: isAlive,
future: node.requestNode(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
@@ -25,6 +27,24 @@ class NodeListRow extends StandardListRow {
}
});
}
@override
Widget buildTrailing(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.newNode,
arguments: {'editingNode': node, 'isSelected': isSelected}),
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context)
.textTheme
.headlineMedium!
.decorationColor!),
child: Icon(Icons.edit,
size: 14,
color: Theme.of(context).textTheme.headlineMedium!.color!)));
}
}
class NodeHeaderListRow extends StandardListRow {
@@ -12,7 +12,6 @@ import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class ConnectionSyncPage extends BasePage {
ConnectionSyncPage(this.nodeListViewModel, this.dashboardViewModel);
@@ -64,49 +63,37 @@ class ConnectionSyncPage extends BasePage {
itemBuilder: (_, sectionIndex, index) {
final node = nodeListViewModel.nodes[index];
final isSelected = node.keyIndex == nodeListViewModel.currentNode.keyIndex;
final nodeListRow = Semantics(
label: 'Slidable',
selected: isSelected,
enabled: !isSelected,
child: NodeListRow(
title: node.uriRaw,
isSelected: isSelected,
isAlive: node.requestNode(),
onTap: (_) async {
if (isSelected) {
return;
}
final nodeListRow = NodeListRow(
title: node.uriRaw,
node: node,
isSelected: isSelected,
onTap: (_) async {
if (isSelected) {
return;
}
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle:
S.of(context).change_current_node_title,
alertContent: nodeListViewModel
.getAlertContent(node.uriRaw),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change,
actionLeftButton: () =>
Navigator.of(context).pop(),
actionRightButton: () async {
await nodeListViewModel.setAsCurrent(node);
Navigator.of(context).pop();
},
);
});
},
),
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle:
S.of(context).change_current_node_title,
alertContent: nodeListViewModel
.getAlertContent(node.uriRaw),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change,
actionLeftButton: () =>
Navigator.of(context).pop(),
actionRightButton: () async {
await nodeListViewModel.setAsCurrent(node);
Navigator.of(context).pop();
},
);
});
},
);
final dismissibleRow = Slidable(
key: Key('${node.keyIndex}'),
startActionPane: _actionPane(context, node, isSelected),
endActionPane: _actionPane(context, node, isSelected),
child: nodeListRow,
);
return dismissibleRow;
return nodeListRow;
},
),
);
@@ -134,44 +121,4 @@ class ConnectionSyncPage extends BasePage {
},
);
}
ActionPane _actionPane(BuildContext context, Node node, bool isSelected) => ActionPane(
motion: const ScrollMotion(),
extentRatio: isSelected ? 0.3 : 0.6,
children: [
if (!isSelected)
SlidableAction(
onPressed: (context) async {
final confirmed = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).remove_node,
alertContent: S.of(context).remove_node_message,
rightButtonText: S.of(context).remove,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.pop(context, true),
actionLeftButton: () => Navigator.pop(context, false));
}) ??
false;
if (confirmed) {
await nodeListViewModel.delete(node);
}
},
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
label: S.of(context).delete,
),
SlidableAction(
onPressed: (_) => Navigator.of(context).pushNamed(Routes.newNode,
arguments: {'editingNode': node, 'isSelected': isSelected}),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
icon: Icons.edit,
label: S.of(context).edit,
),
],
);
}
@@ -0,0 +1,175 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/wallet_name_validator.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_new_vm.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class WalletEditPage extends BasePage {
WalletEditPage(
{required this.walletEditViewModel,
required this.editingWallet,
required this.walletNewVM,
required this.authService})
: _formKey = GlobalKey<FormState>(),
_labelController = TextEditingController(),
super() {
_labelController.text = editingWallet.name;
_labelController.addListener(() => walletEditViewModel.newName = _labelController.text);
}
final GlobalKey<FormState> _formKey;
final TextEditingController _labelController;
final WalletEditViewModel walletEditViewModel;
final WalletNewVM walletNewVM;
final WalletListItem editingWallet;
final AuthService authService;
@override
String get title => S.current.wallet_list_edit_wallet;
Flushbar<void>? _progressBar;
@override
Widget body(BuildContext context) {
return Form(
key: _formKey,
child: Container(
padding: EdgeInsets.all(24.0),
child: Column(
children: <Widget>[
Expanded(
child: Center(
child: BaseTextFormField(
controller: _labelController,
hintText: S.of(context).wallet_list_wallet_name,
validator: WalletNameValidator()))),
Observer(
builder: (_) {
final isLoading = walletEditViewModel.state is WalletEditRenamePending ||
walletEditViewModel.state is WalletEditDeletePending;
return Row(
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(right: 8.0),
child: LoadingPrimaryButton(
isDisabled: isLoading,
onPressed: () => _removeWallet(context),
text: S.of(context).delete,
color: Palette.red,
textColor: Colors.white),
),
),
Flexible(
child: Container(
padding: EdgeInsets.only(left: 8.0),
child: LoadingPrimaryButton(
onPressed: () async {
if (_formKey.currentState?.validate() ?? false) {
if (walletNewVM.nameExists(walletEditViewModel.newName)) {
showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
alertTitle: '',
alertContent: S.of(context).wallet_name_exists,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
} else {
try {
await walletEditViewModel.changeName(editingWallet);
Navigator.of(context).pop();
walletEditViewModel.resetState();
} catch (e) {}
}
}
},
text: S.of(context).save,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
textColor: Colors.white,
isDisabled: walletEditViewModel.newName.isEmpty || isLoading,
),
),
)
],
);
},
)
],
),
),
);
}
Future<void> _removeWallet(BuildContext context) async {
authService.authenticateAction(context, onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully) {
return;
}
_onSuccessfulAuth(context);
});
}
void _onSuccessfulAuth(BuildContext context) async {
bool confirmed = false;
await showPopUp<void>(
context: context,
builder: (BuildContext dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).delete_wallet,
alertContent: S.of(context).delete_wallet_confirm_message(editingWallet.name),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).delete,
actionLeftButton: () => Navigator.of(dialogContext).pop(),
actionRightButton: () {
confirmed = true;
Navigator.of(dialogContext).pop();
});
});
if (confirmed) {
Navigator.of(context).pop();
try {
changeProcessText(context, S.of(context).wallet_list_removing_wallet(editingWallet.name));
await walletEditViewModel.remove(editingWallet);
hideProgressText();
} catch (e) {
changeProcessText(
context,
S.of(context).wallet_list_failed_to_remove(editingWallet.name, e.toString()),
);
}
}
}
void changeProcessText(BuildContext context, String text) {
_progressBar = createBar<void>(text, duration: null)..show(context);
}
Future<void> hideProgressText() async {
await Future.delayed(Duration(milliseconds: 50), () {
_progressBar?.dismiss();
_progressBar = null;
});
}
}
@@ -6,7 +6,6 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:another_flushbar/flushbar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -15,7 +14,6 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
class WalletListPage extends BasePage {
@@ -77,31 +75,7 @@ class WalletListBodyState extends State<WalletListBody> {
? Theme.of(context).accentTextTheme!.titleSmall!.decorationColor!
: Theme.of(context).colorScheme.background;
final row = GestureDetector(
onTap: () async {
if (wallet.isCurrent || !wallet.isEnabled) {
return;
}
final confirmed = await showPopUp<bool>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).change_wallet_alert_title,
alertContent:
S.of(context).change_wallet_alert_content(wallet.name),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change,
actionLeftButton: () =>
Navigator.of(dialogContext).pop(false),
actionRightButton: () =>
Navigator.of(dialogContext).pop(true));
}) ??
false;
if (confirmed) {
await _loadWallet(wallet);
}
},
onTap: () => wallet.isCurrent ? null : _loadWallet(wallet),
child: Container(
height: tileHeight,
width: double.infinity,
@@ -129,16 +103,21 @@ class WalletListBodyState extends State<WalletListBody> {
? _imageFor(type: wallet.type)
: nonWalletTypeIcon,
SizedBox(width: 10),
Text(
wallet.name,
style: TextStyle(
Flexible(
child: Text(
wallet.name,
maxLines: null,
softWrap: true,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.primaryTextTheme
.titleLarge!
.color!),
)
.color!,
),
),
),
],
),
),
@@ -149,12 +128,38 @@ class WalletListBodyState extends State<WalletListBody> {
return wallet.isCurrent
? row
: Slidable(
key: Key('${wallet.key}'),
startActionPane: _actionPane(wallet),
endActionPane: _actionPane(wallet),
child: row,
);
: Row(children: [
Expanded(child: row),
GestureDetector(
onTap: () => Navigator.of(context).pushNamed(
Routes.walletEdit,
arguments: [widget.walletListViewModel, wallet]),
child: Container(
padding: EdgeInsets.only(right: 20),
child: Center(
child: Container(
height: 40,
width: 44,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context)
.textTheme
.headlineMedium!
.decorationColor!),
child: Icon(
Icons.edit,
size: 14,
color: Theme.of(context)
.textTheme
.headlineMedium!
.color!,
),
),
),
),
)
]);
}),
),
),
@@ -226,47 +231,6 @@ class WalletListBodyState extends State<WalletListBody> {
});
}
Future<void> _removeWallet(WalletListItem wallet) async {
widget.authService.authenticateAction(context,
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully) {
return;
}
_onSuccessfulAuth(wallet);
});
}
void _onSuccessfulAuth(WalletListItem wallet) async {
bool confirmed = false;
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).delete_wallet,
alertContent: S.of(context).delete_wallet_confirm_message(wallet.name),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).delete,
actionLeftButton: () => Navigator.of(context).pop(),
actionRightButton: () {
confirmed = true;
Navigator.of(context).pop();
},
);
});
if (confirmed) {
try {
changeProcessText(S.of(context).wallet_list_removing_wallet(wallet.name));
await widget.walletListViewModel.remove(wallet);
hideProgressText();
} catch (e) {
changeProcessText(
S.of(context).wallet_list_failed_to_remove(wallet.name, e.toString()),
);
}
}
}
void changeProcessText(String text) {
_progressBar = createBar<void>(text, duration: null)..show(context);
}
@@ -277,18 +241,4 @@ class WalletListBodyState extends State<WalletListBody> {
_progressBar = null;
});
}
ActionPane _actionPane(WalletListItem wallet) => ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.3,
children: [
SlidableAction(
onPressed: (_) => _removeWallet(wallet),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
label: S.of(context).delete,
),
],
);
}
+1 -1
View File
@@ -231,7 +231,7 @@ class SectionStandardList extends StatelessWidget {
return Container();
}
return StandardListSeparator(padding: EdgeInsets.only(left: 24));
return StandardListSeparator(padding: dividerPadding);
},
itemCount: totalRows.length,
itemBuilder: (_, index) => totalRows[index]);