desktop node management ui

This commit is contained in:
julian 2022-11-03 17:53:19 -06:00
parent 14d7c443f2
commit bed25b37f7
6 changed files with 762 additions and 388 deletions

View file

@ -8,7 +8,6 @@ import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/node_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -20,15 +19,18 @@ import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:uuid/uuid.dart';
import 'package:stackwallet/utilities/util.dart';
enum AddEditNodeViewType { add, edit }
class AddEditNodeView extends ConsumerStatefulWidget {
@ -59,6 +61,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
late final AddEditNodeViewType viewType;
late final Coin coin;
late final String? nodeId;
late final bool isDesktop;
late bool saveEnabled;
late bool testConnectionEnabled;
@ -162,8 +165,198 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
return testPassed;
}
Future<void> attemptSave() async {
final canConnect = await _testConnection(showFlushBar: false);
bool? shouldSave;
if (!canConnect) {
await showDialog<dynamic>(
context: context,
useSafeArea: true,
barrierDismissible: true,
builder: (_) => isDesktop
? DesktopDialog(
maxWidth: 440,
maxHeight: 300,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 32,
),
child: Row(
children: [
const SizedBox(
width: 32,
),
Text(
"Server currently unreachable",
style: STextStyles.desktopH3(context),
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
top: 16,
bottom: 32,
),
child: Column(
children: [
const Spacer(),
Text(
"Would you like to save this node anyways?",
style: STextStyles.desktopTextMedium(context),
),
const Spacer(
flex: 2,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
desktopMed: true,
onPressed: () => Navigator.of(
context,
rootNavigator: true,
).pop(false),
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Save",
desktopMed: true,
onPressed: () => Navigator.of(
context,
rootNavigator: true,
).pop(true),
),
),
],
),
],
),
),
),
],
),
)
: StackDialog(
title: "Server currently unreachable",
message: "Would you like to save this node anyways?",
leftButton: TextButton(
onPressed: () async {
Navigator.of(context).pop(false);
},
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
rightButton: TextButton(
onPressed: () async {
Navigator.of(context).pop(true);
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
child: Text(
"Save",
style: STextStyles.button(context),
),
),
),
).then((value) {
if (value is bool && value) {
shouldSave = true;
} else {
shouldSave = false;
}
});
}
if (!canConnect && !shouldSave!) {
// return without saving
return;
}
final formData = ref.read(nodeFormDataProvider);
// strip unused path
String address = formData.host!;
if (coin == Coin.monero || coin == Coin.wownero || coin == Coin.epicCash) {
if (address.startsWith("http")) {
final uri = Uri.parse(address);
address = "${uri.scheme}://${uri.host}";
}
}
switch (viewType) {
case AddEditNodeViewType.add:
NodeModel node = NodeModel(
host: address,
port: formData.port!,
name: formData.name!,
id: const Uuid().v1(),
useSSL: formData.useSSL!,
loginName: formData.login,
enabled: true,
coinName: coin.name,
isFailover: formData.isFailover!,
isDown: false,
);
await ref.read(nodeServiceChangeNotifierProvider).add(
node,
formData.password,
true,
);
if (mounted) {
Navigator.of(context)
.popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete));
}
break;
case AddEditNodeViewType.edit:
NodeModel node = NodeModel(
host: address,
port: formData.port!,
name: formData.name!,
id: nodeId!,
useSSL: formData.useSSL!,
loginName: formData.login,
enabled: true,
coinName: coin.name,
isFailover: formData.isFailover!,
isDown: false,
);
await ref.read(nodeServiceChangeNotifierProvider).add(
node,
formData.password,
true,
);
if (mounted) {
Navigator.of(context)
.popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete));
}
break;
}
}
@override
void initState() {
isDesktop = Util.isDesktop;
ref.refresh(nodeFormDataProvider);
viewType = widget.viewType;
@ -196,7 +389,9 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
.select((value) => value.getNodeById(id: nodeId!)))
: null;
return Scaffold(
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
@ -228,7 +423,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
key: const Key("deleteNodeAppBarButtonKey"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
color:
Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.trash,
color: Theme.of(context)
@ -267,6 +463,49 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
constraints:
BoxConstraints(minHeight: constraints.maxHeight - 8),
child: IntrinsicHeight(
child: child,
),
),
),
);
},
),
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopDialog(
maxWidth: 580,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const SizedBox(
width: 8,
),
const AppBarBackButton(
iconSize: 24,
size: 40,
),
Text(
"Add new node",
style: STextStyles.desktopH3(context),
)
],
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
top: 16,
bottom: 32,
),
child: child,
),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -293,30 +532,45 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
}
},
),
const Spacer(),
TextButton(
if (!isDesktop) const Spacer(),
if (isDesktop)
const SizedBox(
height: 78,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Test connection",
enabled: testConnectionEnabled,
desktopMed: true,
onPressed: testConnectionEnabled
? () async {
await _testConnection();
}
: null,
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Text(
"Test connection",
style: STextStyles.button(context).copyWith(
color: testConnectionEnabled
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textWhite,
),
),
if (isDesktop)
const SizedBox(
width: 16,
),
const SizedBox(height: 16),
if (isDesktop)
Expanded(
child: PrimaryButton(
label: "Save",
enabled: saveEnabled,
desktopMed: true,
onPressed: saveEnabled ? attemptSave : null,
),
),
],
),
if (!isDesktop)
const SizedBox(
height: 16,
),
if (!isDesktop)
TextButton(
style: saveEnabled
? Theme.of(context)
@ -325,138 +579,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonColor(context),
onPressed: saveEnabled
? () async {
final canConnect = await _testConnection(
showFlushBar: false);
bool? shouldSave;
if (!canConnect) {
await showDialog<dynamic>(
context: context,
useSafeArea: true,
barrierDismissible: true,
builder: (_) => StackDialog(
title: "Server currently unreachable",
message:
"Would you like to save this node anyways?",
leftButton: TextButton(
onPressed: () async {
Navigator.of(context).pop(false);
},
child: Text(
"Cancel",
style: STextStyles.button(context)
.copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.accentColorDark),
),
),
rightButton: TextButton(
onPressed: () async {
Navigator.of(context).pop(true);
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(
context),
child: Text(
"Save",
style: STextStyles.button(context),
),
),
),
).then((value) {
if (value is bool && value) {
shouldSave = true;
} else {
shouldSave = false;
}
});
}
if (!canConnect && !shouldSave!) {
// return without saving
return;
}
final formData =
ref.read(nodeFormDataProvider);
// strip unused path
String address = formData.host!;
if (coin == Coin.monero ||
coin == Coin.wownero ||
coin == Coin.epicCash) {
if (address.startsWith("http")) {
final uri = Uri.parse(address);
address = "${uri.scheme}://${uri.host}";
}
}
switch (viewType) {
case AddEditNodeViewType.add:
NodeModel node = NodeModel(
host: address,
port: formData.port!,
name: formData.name!,
id: const Uuid().v1(),
useSSL: formData.useSSL!,
loginName: formData.login,
enabled: true,
coinName: coin.name,
isFailover: formData.isFailover!,
isDown: false,
);
await ref
.read(
nodeServiceChangeNotifierProvider)
.add(
node,
formData.password,
true,
);
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(
widget.routeOnSuccessOrDelete));
}
break;
case AddEditNodeViewType.edit:
NodeModel node = NodeModel(
host: address,
port: formData.port!,
name: formData.name!,
id: nodeId!,
useSSL: formData.useSSL!,
loginName: formData.login,
enabled: true,
coinName: coin.name,
isFailover: formData.isFailover!,
isDown: false,
);
await ref
.read(
nodeServiceChangeNotifierProvider)
.add(
node,
formData.password,
true,
);
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(
widget.routeOnSuccessOrDelete));
}
break;
}
}
: null,
onPressed: saveEnabled ? attemptSave : null,
child: Text(
"Save",
style: STextStyles.button(context),
@ -465,12 +588,6 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
],
),
),
),
),
);
},
),
),
);
}
}

View file

@ -9,6 +9,7 @@ import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:tuple/tuple.dart';
@ -17,11 +18,13 @@ class CoinNodesView extends ConsumerStatefulWidget {
const CoinNodesView({
Key? key,
required this.coin,
this.rootNavigator = false,
}) : super(key: key);
static const String routeName = "/coinNodes";
final Coin coin;
final bool rootNavigator;
@override
ConsumerState<CoinNodesView> createState() => _CoinNodesViewState();
@ -63,12 +66,17 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> {
textAlign: TextAlign.center,
),
Expanded(
child: const DesktopDialogCloseButton(),
child: DesktopDialogCloseButton(
onPressedOverride: Navigator.of(
context,
rootNavigator: widget.rootNavigator,
).pop,
),
),
],
),
Padding(
padding: EdgeInsets.only(
padding: const EdgeInsets.only(
left: 32,
right: 32,
),
@ -83,14 +91,19 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> {
),
textAlign: TextAlign.left,
),
RichText(
text: TextSpan(
text: 'Add new nodes',
style:
STextStyles.desktopTextExtraSmall(context).copyWith(
color: Colors.blueAccent,
),
BlueTextButton(
text: "Add new node",
onTap: () {
Navigator.of(context).pushNamed(
AddEditNodeView.routeName,
arguments: Tuple4(
AddEditNodeViewType.add,
widget.coin,
null,
CoinNodesView.routeName,
),
);
},
),
],
),

View file

@ -17,7 +17,13 @@ import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/delete_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:tuple/tuple.dart';
class NodeDetailsView extends ConsumerStatefulWidget {
@ -48,6 +54,8 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
late final String nodeId;
late final String popRouteName;
bool _desktopReadOnly = true;
@override
initState() {
secureStore = widget.secureStore;
@ -126,23 +134,34 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
}
if (testPassed) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Server ping success",
context: context,
),
);
} else {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Server unreachable",
context: context,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
final isDesktop = Util.isDesktop;
final node = ref.watch(nodeServiceChangeNotifierProvider
.select((value) => value.getNodeById(id: nodeId)));
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
@ -174,7 +193,8 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
key: const Key("nodeDetailsEditNodeAppBarButtonKey"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
color:
Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.pencil,
color: Theme.of(context)
@ -207,9 +227,6 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
),
child: LayoutBuilder(
builder: (context, constraints) {
final node = ref.watch(nodeServiceChangeNotifierProvider
.select((value) => value.getNodeById(id: nodeId)));
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(4),
@ -217,41 +234,141 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
constraints:
BoxConstraints(minHeight: constraints.maxHeight - 8),
child: IntrinsicHeight(
child: child,
),
),
),
);
},
),
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopDialog(
maxWidth: 580,
maxHeight: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const SizedBox(
width: 8,
),
const AppBarBackButton(
iconSize: 24,
size: 40,
),
Text(
"Node details",
style: STextStyles.desktopH3(context),
)
],
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
top: 16,
bottom: 32,
),
child: child,
),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NodeForm(
node: node,
secureStore: secureStore,
readOnly: true,
readOnly: isDesktop ? _desktopReadOnly : true,
coin: coin,
),
const Spacer(),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
if (!isDesktop) const Spacer(),
if (isDesktop)
const SizedBox(
height: 22,
),
if (isDesktop)
SizedBox(
height: 56,
child: _desktopReadOnly
? null
: Row(
children: [
Expanded(
child: DeleteButton(
label: "Delete node",
desktopMed: true,
onPressed: () async {
await _testConnection(ref, context);
Navigator.of(context).pop();
await ref
.read(nodeServiceChangeNotifierProvider)
.delete(
node!.id,
true,
);
},
child: Text(
"Test connection",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
const SizedBox(height: 16),
const SizedBox(
width: 16,
),
const Spacer(),
],
),
),
if (isDesktop && !_desktopReadOnly)
const SizedBox(
height: 45,
),
),
);
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Test connection",
desktopMed: true,
onPressed: () async {
await _testConnection(ref, context);
},
),
),
if (isDesktop)
const SizedBox(
width: 16,
),
if (isDesktop)
Expanded(
child: !nodeId.startsWith("default")
? PrimaryButton(
label: _desktopReadOnly ? "Edit" : "Save",
desktopMed: true,
onPressed: () async {
final shouldSave = _desktopReadOnly == false;
setState(() {
_desktopReadOnly = !_desktopReadOnly;
});
if (shouldSave) {
// todo save node
}
},
)
: Container(),
),
],
),
if (!isDesktop)
const SizedBox(
height: 16,
),
],
),
),
);
}
}

View file

@ -4,6 +4,7 @@ import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart';
import 'package:stackwallet/providers/global/node_service_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -167,8 +168,22 @@ class _NodesSettings extends ConsumerState<NodesSettings> {
onPressed: () {
showDialog<void>(
context: context,
builder: (context) => CoinNodesView(
builder: (context) => Navigator(
initialRoute: CoinNodesView.routeName,
onGenerateRoute: RouteGenerator.generateRoute,
onGenerateInitialRoutes: (_, __) {
return [
FadePageRoute(
CoinNodesView(
coin: coin,
rootNavigator: true,
),
const RouteSettings(
name: CoinNodesView.routeName,
),
),
];
},
),
);
},

View file

@ -1468,6 +1468,20 @@ class StackColors extends ThemeExtension<StackColors> {
}
}
ButtonStyle? getDeleteEnabledButtonColor(BuildContext context) =>
Theme.of(context).textButtonTheme.style?.copyWith(
backgroundColor: MaterialStateProperty.all<Color>(
textFieldErrorBG,
),
);
ButtonStyle? getDeleteDisabledButtonColor(BuildContext context) =>
Theme.of(context).textButtonTheme.style?.copyWith(
backgroundColor: MaterialStateProperty.all<Color>(
buttonBackSecondaryDisabled,
),
);
ButtonStyle? getPrimaryEnabledButtonColor(BuildContext context) =>
Theme.of(context).textButtonTheme.style?.copyWith(
backgroundColor: MaterialStateProperty.all<Color>(

View file

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/custom_text_button.dart';
class DeleteButton extends StatelessWidget {
const DeleteButton({
Key? key,
this.width,
this.height,
this.label,
this.onPressed,
this.enabled = true,
this.desktopMed = false,
}) : super(key: key);
final double? width;
final double? height;
final String? label;
final VoidCallback? onPressed;
final bool enabled;
final bool desktopMed;
TextStyle getStyle(bool isDesktop, BuildContext context) {
if (isDesktop) {
if (desktopMed) {
return STextStyles.desktopTextExtraSmall(context).copyWith(
color: enabled
? Theme.of(context).extension<StackColors>()!.accentColorRed
: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondaryDisabled,
);
} else {
return enabled
? STextStyles.desktopButtonSecondaryEnabled(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.accentColorRed)
: STextStyles.desktopButtonSecondaryDisabled(context);
}
} else {
return STextStyles.button(context).copyWith(
color: enabled
? Theme.of(context).extension<StackColors>()!.accentColorRed
: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondaryDisabled,
);
}
}
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
return CustomTextButtonBase(
height: desktopMed ? 56 : height,
width: width,
textButton: TextButton(
onPressed: enabled ? onPressed : null,
style: enabled
? Theme.of(context)
.extension<StackColors>()!
.getDeleteEnabledButtonColor(context)
: Theme.of(context)
.extension<StackColors>()!
.getDeleteDisabledButtonColor(context),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
Assets.svg.trash,
width: 20,
height: 20,
color: enabled
? Theme.of(context).extension<StackColors>()!.accentColorRed
: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondaryDisabled,
),
if (label != null)
const SizedBox(
width: 10,
),
if (label != null)
Text(
label!,
style: getStyle(isDesktop, context),
),
],
),
),
);
}
}