LCW-1191-ios-node-qr-issue (#2568)

* refactor: improve `NodeCreateOrEditPage` and `NodeForm` structure and state management * fix: eliminate `widget.nodeViewModel` references in constructor

Konstantin Ullrich committed Oct 8, 2025 at 15:59 UTC 82dfd6afa2c7e8b98db6e5d70a005156a10c40c1
2 files changed +249 -313
lib/src/screens/nodes/node_create_or_edit_page.dart
+109 -161
@@ -1,91 +1,45 @@
1 import 'package:cake_wallet/core/execution_state.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 +import 'package:cake_wallet/src/screens/base_page.dart';
4 import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart';
5 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
6 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
7 +import 'package:cake_wallet/src/widgets/primary_button.dart';
8 +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
9 import 'package:cake_wallet/utils/show_pop_up.dart';
10 +import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
11 import 'package:cw_core/node.dart';
12 import 'package:cw_core/wallet_type.dart';
13 import 'package:flutter/material.dart';
14 import 'package:flutter_mobx/flutter_mobx.dart';
15 import 'package:mobx/mobx.dart';
11 -import 'package:cake_wallet/generated/i18n.dart';
12 -import 'package:cake_wallet/src/widgets/primary_button.dart';
13 -import 'package:cake_wallet/src/screens/base_page.dart';
14 -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
15 -import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
16
17 class NodeCreateOrEditPage extends BasePage {
18 - NodeCreateOrEditPage(
19 - {required this.nodeCreateOrEditViewModel, this.editingNode, this.isSelected, this.type})
20 - : _formKey = GlobalKey<FormState>(),
21 - _addressController = TextEditingController(),
22 - _pathController = TextEditingController(),
23 - _portController = TextEditingController(),
24 - _loginController = TextEditingController(),
25 - _passwordController = TextEditingController() {
26 - reaction((_) => nodeCreateOrEditViewModel.address, (String address) {
27 - if (address != _addressController.text) {
28 - _addressController.text = address;
29 - }
30 - });
31 -
32 - reaction((_) => nodeCreateOrEditViewModel.port, (String port) {
33 - if (port != _portController.text) {
34 - _portController.text = port;
35 - }
36 - });
37 -
38 - if (nodeCreateOrEditViewModel.hasAuthCredentials) {
39 - reaction((_) => nodeCreateOrEditViewModel.login, (String login) {
40 - if (login != _loginController.text) {
41 - _loginController.text = login;
42 - }
43 - });
44 -
45 - reaction((_) => nodeCreateOrEditViewModel.password, (String password) {
46 - if (password != _passwordController.text) {
47 - _passwordController.text = password;
48 - }
49 - });
50 - }
51 -
52 - _addressController
53 - .addListener(() => nodeCreateOrEditViewModel.address = _addressController.text);
54 - _pathController.addListener(() => nodeCreateOrEditViewModel.path = _pathController.text);
55 - _portController.addListener(() => nodeCreateOrEditViewModel.port = _portController.text);
56 - _loginController.addListener(() => nodeCreateOrEditViewModel.login = _loginController.text);
57 - _passwordController
58 - .addListener(() => nodeCreateOrEditViewModel.password = _passwordController.text);
59 - }
18 + NodeCreateOrEditPage({
19 + required this.nodeCreateOrEditViewModel,
20 + this.editingNode,
21 + this.isSelected,
22 + this.type,
23 + }) : _formKey = GlobalKey<FormState>();
24
25 final GlobalKey<FormState> _formKey;
62 - final TextEditingController _addressController;
63 - final TextEditingController _pathController;
64 - final TextEditingController _portController;
65 - final TextEditingController _loginController;
66 - final TextEditingController _passwordController;
26 + final NodeCreateOrEditViewModel nodeCreateOrEditViewModel;
27 + final Node? editingNode;
28 + final bool? isSelected;
29 + final WalletType? type;
30
31 @override
32 String get title => editingNode != null ? S.current.edit_node : S.current.node_new;
33
34 @override
35 Widget trailing(BuildContext context) => IconButton(
73 - onPressed: () async {
74 - await nodeCreateOrEditViewModel.scanQRCodeForNewNode(context);
75 - },
36 + onPressed: () => nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
37 splashColor: Colors.transparent,
38 highlightColor: Colors.transparent,
39 hoverColor: Colors.transparent,
79 - icon: Image.asset(
80 - 'assets/images/qr_code_icon.png',
81 - ),
40 + icon: Image.asset('assets/images/qr_code_icon.png'),
41 );
42
84 - final NodeCreateOrEditViewModel nodeCreateOrEditViewModel;
85 - final Node? editingNode;
86 - final bool? isSelected;
87 - final WalletType? type;
88 -
43 @override
44 Widget body(BuildContext context) {
45 reaction(
@@ -93,116 +47,110 @@ class NodeCreateOrEditPage extends BasePage {
47 (ExecutionState state) {
48 if (state is ExecutedSuccessfullyState) {
49 WidgetsBinding.instance.addPostFrameCallback(
96 - (_) {
97 - showPopUp<void>(
98 - context: context,
99 - builder: (BuildContext context) => AlertWithOneAction(
100 - alertTitle: S.of(context).new_node_testing,
101 - alertContent: state.payload as bool
102 - ? S.of(context).node_connection_successful
103 - : S.of(context).node_connection_failed,
104 - buttonText: S.of(context).ok,
105 - buttonAction: () => Navigator.of(context).pop(),
106 - ),
107 - );
108 - },
50 + (_) => showPopUp<void>(
51 + context: context,
52 + builder: (context) => AlertWithOneAction(
53 + alertTitle: S.of(context).new_node_testing,
54 + alertContent: state.payload as bool
55 + ? S.of(context).node_connection_successful
56 + : S.of(context).node_connection_failed,
57 + buttonText: S.of(context).ok,
58 + buttonAction: () => Navigator.of(context).pop(),
59 + ),
60 + ),
61 );
62 }
63
64 if (state is FailureState) {
65 WidgetsBinding.instance.addPostFrameCallback(
114 - (_) {
115 - showPopUp<void>(
116 - context: context,
117 - builder: (BuildContext context) {
118 - return AlertWithOneAction(
119 - alertTitle: S.of(context).error,
120 - alertContent: state.error,
121 - buttonText: S.of(context).ok,
122 - buttonAction: () => Navigator.of(context).pop(),
123 - );
124 - },
125 - );
126 - },
66 + (_) => showPopUp<void>(
67 + context: context,
68 + builder: (context) => AlertWithOneAction(
69 + alertTitle: S.of(context).error,
70 + alertContent: state.error,
71 + buttonText: S.of(context).ok,
72 + buttonAction: () => Navigator.of(context).pop(),
73 + ),
74 + ),
75 );
76 }
77 },
78 );
79
80 return Container(
133 - padding: EdgeInsets.only(left: 24, right: 24),
134 - child: ScrollableWithBottomSection(
135 - contentPadding: EdgeInsets.only(bottom: 24.0, top: 8),
136 - content: NodeForm(
137 - formKey: _formKey,
138 - nodeViewModel: nodeCreateOrEditViewModel,
139 - editingNode: editingNode,
140 - type: type,
141 - ),
142 - bottomSectionPadding: EdgeInsets.only(bottom: 24),
143 - bottomSection: Observer(
144 - builder: (_) => Row(
145 - mainAxisAlignment: MainAxisAlignment.center,
146 - children: <Widget>[
147 - Flexible(
148 - child: Container(
149 - padding: EdgeInsets.only(right: 8.0),
150 - child: LoadingPrimaryButton(
151 - onPressed: () async {
152 - final confirmed = await showPopUp<bool>(
153 - context: context,
154 - builder: (BuildContext context) {
155 - return AlertWithTwoActions(
156 - alertTitle: S.of(context).remove_node,
157 - alertContent: S.of(context).remove_node_message,
158 - rightButtonText: S.of(context).remove,
159 - leftButtonText: S.of(context).cancel,
160 - actionRightButton: () => Navigator.pop(context, true),
161 - actionLeftButton: () => Navigator.pop(context, false),
162 - );
163 - },
164 - ) ??
165 - false;
81 + padding: const EdgeInsets.only(left: 24, right: 24),
82 + child: ScrollableWithBottomSection(
83 + contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8),
84 + content: NodeForm(
85 + formKey: _formKey,
86 + nodeViewModel: nodeCreateOrEditViewModel,
87 + editingNode: editingNode,
88 + type: type,
89 + ),
90 + bottomSectionPadding: const EdgeInsets.only(bottom: 24),
91 + bottomSection: Observer(
92 + builder: (_) => Row(
93 + mainAxisAlignment: MainAxisAlignment.center,
94 + children: <Widget>[
95 + Flexible(
96 + child: Container(
97 + padding: const EdgeInsets.only(right: 8.0),
98 + child: LoadingPrimaryButton(
99 + onPressed: () async {
100 + final confirmed = await showPopUp<bool>(
101 + context: context,
102 + builder: (context) => AlertWithTwoActions(
103 + alertTitle: S.of(context).remove_node,
104 + alertContent: S.of(context).remove_node_message,
105 + rightButtonText: S.of(context).remove,
106 + leftButtonText: S.of(context).cancel,
107 + actionRightButton: () => Navigator.pop(context, true),
108 + actionLeftButton: () => Navigator.pop(context, false),
109 + ),
110 + ) ??
111 + false;
112
167 - if (confirmed) {
168 - await editingNode!.delete();
169 - Navigator.of(context).pop();
170 - }
171 - },
172 - text: S.of(context).delete,
173 - isDisabled: editingNode == null ||
174 - !nodeCreateOrEditViewModel.isReady ||
175 - (isSelected ?? false),
176 - color: Theme.of(context).colorScheme.errorContainer,
177 - textColor: Theme.of(context).colorScheme.onErrorContainer,
178 - ),
179 - ),
180 - ),
181 - Flexible(
182 - child: Container(
183 - padding: EdgeInsets.only(left: 8.0),
184 - child: PrimaryButton(
185 - onPressed: () async {
186 - if (_formKey.currentState != null &&
187 - !_formKey.currentState!.validate()) {
188 - return;
189 - }
113 + if (confirmed) {
114 + await editingNode!.delete();
115 + Navigator.of(context).pop();
116 + }
117 + },
118 + text: S.of(context).delete,
119 + isDisabled: editingNode == null ||
120 + !nodeCreateOrEditViewModel.isReady ||
121 + (isSelected ?? false),
122 + color: Theme.of(context).colorScheme.errorContainer,
123 + textColor: Theme.of(context).colorScheme.onErrorContainer,
124 + ),
125 + ),
126 + ),
127 + Flexible(
128 + child: Container(
129 + padding: const EdgeInsets.only(left: 8.0),
130 + child: PrimaryButton(
131 + onPressed: () async {
132 + if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
133 + return;
134 + }
135
191 - await nodeCreateOrEditViewModel.save(
192 - editingNode: editingNode, saveAsCurrent: isSelected ?? false);
193 - if (context.mounted) {
194 - Navigator.of(context).pop();
195 - }
196 - },
197 - text: S.of(context).save,
198 - color: Theme.of(context).colorScheme.primary,
199 - textColor: Theme.of(context).colorScheme.onPrimary,
200 - isDisabled: (!nodeCreateOrEditViewModel.isReady) ||
201 - (nodeCreateOrEditViewModel.connectionState is IsExecutingState),
202 - ),
203 - )),
204 - ],
205 - )),
206 - ));
136 + await nodeCreateOrEditViewModel.save(
137 + editingNode: editingNode, saveAsCurrent: isSelected ?? false);
138 + if (context.mounted) {
139 + Navigator.of(context).pop();
140 + }
141 + },
142 + text: S.of(context).save,
143 + color: Theme.of(context).colorScheme.primary,
144 + textColor: Theme.of(context).colorScheme.onPrimary,
145 + isDisabled: (!nodeCreateOrEditViewModel.isReady) ||
146 + (nodeCreateOrEditViewModel.connectionState is IsExecutingState),
147 + ),
148 + ),
149 + ),
150 + ],
151 + ),
152 + ),
153 + ),
154 + );
155 }
156 }
lib/src/screens/nodes/widgets/node_form.dart
+140 -152
@@ -1,6 +1,7 @@
1 import 'package:cake_wallet/core/node_address_validator.dart';
2 import 'package:cake_wallet/core/node_port_validator.dart';
3 import 'package:cake_wallet/core/socks_proxy_node_address_validator.dart';
4 +import 'package:cake_wallet/generated/i18n.dart';
5 import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
6 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
7 import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
@@ -8,15 +9,30 @@ import 'package:cw_core/node.dart';
9 import 'package:cw_core/wallet_type.dart';
10 import 'package:flutter/material.dart';
11 import 'package:flutter_mobx/flutter_mobx.dart';
11 -import 'package:cake_wallet/generated/i18n.dart';
12 import 'package:mobx/mobx.dart';
13
14 -class NodeForm extends StatelessWidget {
14 +class NodeForm extends StatefulWidget {
15 NodeForm({
16 required this.nodeViewModel,
17 required this.formKey,
18 this.editingNode,
19 this.type,
20 + });
21 +
22 + final NodeCreateOrEditViewModel nodeViewModel;
23 + final GlobalKey<FormState> formKey;
24 + final Node? editingNode;
25 + final WalletType? type;
26 +
27 + @override
28 + State<StatefulWidget> createState() =>
29 + _NodeFormState(nodeViewModel: nodeViewModel, editingNode: this.editingNode);
30 +}
31 +
32 +class _NodeFormState extends State<NodeForm> {
33 + _NodeFormState({
34 + required this.nodeViewModel,
35 + Node? editingNode,
36 }) : _addressController = TextEditingController(text: editingNode?.uri.host.toString()),
37 _pathController = TextEditingController(text: editingNode?.path.toString()),
38 _portController = TextEditingController(
@@ -28,28 +44,25 @@ class NodeForm extends StatelessWidget {
44 _socksAddressController = TextEditingController(text: editingNode?.socksProxyAddress) {
45 if (editingNode != null) {
46 nodeViewModel
31 - ..setAddress((editingNode!.uri.host.toString()))
32 - ..setPath((editingNode!.path.toString()))
33 - ..setPort((editingNode!.uri.hasPort ? editingNode!.uri.port.toString() : ''))
34 - ..setPassword((editingNode!.password ?? ''))
35 - ..setLogin((editingNode!.login ?? ''))
36 - ..setSSL((editingNode!.isSSL))
37 - ..setTrusted((editingNode!.trusted))
38 - ..setIsEnabledForAutoSwitching((editingNode!.isEnabledForAutoSwitching))
39 - ..setSocksProxy((editingNode!.useSocksProxy))
40 - ..setSocksProxyAddress((editingNode!.socksProxyAddress ?? ''));
47 + ..setAddress(editingNode.uri.host.toString())
48 + ..setPath(editingNode.path.toString())
49 + ..setPort(editingNode.uri.hasPort ? editingNode.uri.port.toString() : '')
50 + ..setPassword(editingNode.password ?? '')
51 + ..setLogin(editingNode.login ?? '')
52 + ..setSSL(editingNode.isSSL)
53 + ..setTrusted(editingNode.trusted)
54 + ..setIsEnabledForAutoSwitching(editingNode.isEnabledForAutoSwitching)
55 + ..setSocksProxy(editingNode.useSocksProxy)
56 + ..setSocksProxyAddress(editingNode.socksProxyAddress ?? '');
57 }
58 +
59 if (nodeViewModel.hasAuthCredentials) {
60 reaction((_) => nodeViewModel.login, (String login) {
44 - if (login != _loginController.text) {
45 - _loginController.text = login;
46 - }
61 + if (login != _loginController.text) _loginController.text = login;
62 });
63
64 reaction((_) => nodeViewModel.password, (String password) {
50 - if (password != _passwordController.text) {
51 - _passwordController.text = password;
52 - }
65 + if (password != _passwordController.text) _passwordController.text = password;
66 });
67 }
68 reaction((_) => nodeViewModel.address, (String address) {
@@ -59,15 +72,11 @@ class NodeForm extends StatelessWidget {
72 });
73
74 reaction((_) => nodeViewModel.port, (String port) {
62 - if (port != _portController.text) {
63 - _portController.text = port;
64 - }
75 + if (port != _portController.text) _portController.text = port;
76 });
77
78 reaction((_) => nodeViewModel.path, (String path) {
68 - if (path != _pathController.text) {
69 - _pathController.text = path;
70 - }
79 + if (path != _pathController.text) _pathController.text = path;
80 });
81
82 _addressController.addListener(() => nodeViewModel.address = _addressController.text);
@@ -80,9 +89,6 @@ class NodeForm extends StatelessWidget {
89 }
90
91 final NodeCreateOrEditViewModel nodeViewModel;
83 - final GlobalKey<FormState> formKey;
84 - final Node? editingNode;
85 - final WalletType? type;
92
93 final TextEditingController _addressController;
94 final TextEditingController _pathController;
@@ -92,54 +98,46 @@ class NodeForm extends StatelessWidget {
98 final TextEditingController _socksAddressController;
99
100 @override
95 - Widget build(BuildContext context) {
96 - return Form(
97 - key: formKey,
98 - child: Column(
99 - children: <Widget>[
100 - Row(
101 - children: <Widget>[
102 - Expanded(
103 - child: BaseTextFormField(
104 - controller: _addressController,
105 - hintText: S.of(context).node_address,
106 - validator: type == WalletType.decred
107 - ? NodeAddressValidatorDecredBlankException()
108 - : NodeAddressValidator(),
109 - ),
110 - )
111 - ],
112 - ),
113 - SizedBox(height: 10.0),
101 + Widget build(BuildContext context) => Form(
102 + key: widget.formKey,
103 + child: Column(children: <Widget>[
104 + Row(children: <Widget>[
105 + Expanded(
106 + child: BaseTextFormField(
107 + controller: _addressController,
108 + hintText: S.of(context).node_address,
109 + validator: widget.type == WalletType.decred
110 + ? NodeAddressValidatorDecredBlankException()
111 + : NodeAddressValidator(),
112 + ),
113 + )
114 + ]),
115 + const SizedBox(height: 10),
116 if (nodeViewModel.hasPathSupport) ...[
115 - Row(
116 - children: <Widget>[
117 - Expanded(
118 - child: BaseTextFormField(
119 - controller: _pathController,
120 - hintText: "/path",
121 - validator: NodePathValidator(),
122 - ),
123 - )
124 - ],
125 - ),
126 - SizedBox(height: 10.0),
127 - ],
128 - Row(
129 - children: <Widget>[
117 + Row(children: <Widget>[
118 Expanded(
119 child: BaseTextFormField(
132 - controller: _portController,
133 - hintText: S.of(context).node_port,
134 - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
135 - validator: NodePortValidator(),
120 + controller: _pathController,
121 + hintText: "/path",
122 + validator: NodePathValidator(),
123 ),
124 )
138 - ],
139 - ),
140 - SizedBox(height: 10.0),
125 + ]),
126 + const SizedBox(height: 10),
127 + ],
128 + Row(children: <Widget>[
129 + Expanded(
130 + child: BaseTextFormField(
131 + controller: _portController,
132 + hintText: S.of(context).node_port,
133 + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
134 + validator: NodePortValidator(),
135 + ),
136 + )
137 + ]),
138 + const SizedBox(height: 10),
139 Padding(
142 - padding: EdgeInsets.only(top: 20),
140 + padding: const EdgeInsets.only(top: 20),
141 child: Row(
142 mainAxisAlignment: MainAxisAlignment.start,
143 mainAxisSize: MainAxisSize.max,
@@ -156,9 +154,9 @@ class NodeForm extends StatelessWidget {
154 ],
155 ),
156 ),
159 - SizedBox(height: 10.0),
157 + const SizedBox(height: 10),
158 Padding(
161 - padding: EdgeInsets.only(top: 20),
159 + padding: const EdgeInsets.only(top: 20),
160 child: Row(
161 mainAxisAlignment: MainAxisAlignment.start,
162 mainAxisSize: MainAxisSize.max,
@@ -175,31 +173,27 @@ class NodeForm extends StatelessWidget {
173 ],
174 ),
175 ),
178 - SizedBox(height: 10.0),
176 + const SizedBox(height: 10),
177 if (nodeViewModel.hasAuthCredentials) ...[
180 - Row(
181 - children: <Widget>[
182 - Expanded(
183 - child: BaseTextFormField(
184 - controller: _loginController,
185 - hintText: S.of(context).login,
186 - ),
187 - )
188 - ],
189 - ),
190 - SizedBox(height: 10.0),
191 - Row(
192 - children: <Widget>[
193 - Expanded(
194 - child: BaseTextFormField(
195 - controller: _passwordController,
196 - hintText: S.of(context).password,
197 - ),
198 - )
199 - ],
200 - ),
178 + Row(children: <Widget>[
179 + Expanded(
180 + child: BaseTextFormField(
181 + controller: _loginController,
182 + hintText: S.of(context).login,
183 + ),
184 + )
185 + ]),
186 + const SizedBox(height: 10),
187 + Row(children: <Widget>[
188 + Expanded(
189 + child: BaseTextFormField(
190 + controller: _passwordController,
191 + hintText: S.of(context).password,
192 + ),
193 + )
194 + ]),
195 Padding(
202 - padding: EdgeInsets.only(top: 20),
196 + padding: const EdgeInsets.only(top: 20),
197 child: Row(
198 mainAxisAlignment: MainAxisAlignment.start,
199 mainAxisSize: MainAxisSize.max,
@@ -217,66 +211,60 @@ class NodeForm extends StatelessWidget {
211 ),
212 ),
213 Observer(
220 - builder: (_) => Column(
214 + builder: (_) => Column(children: [
215 + if (nodeViewModel.usesEmbeddedProxy) ...[
216 + Padding(
217 + padding: const EdgeInsets.only(top: 10),
218 + child: Row(
219 + mainAxisAlignment: MainAxisAlignment.start,
220 + mainAxisSize: MainAxisSize.max,
221 children: [
222 - if (nodeViewModel.usesEmbeddedProxy) ...[
223 - Padding(
224 - padding: EdgeInsets.only(top: 10),
225 - child: Row(
226 - mainAxisAlignment: MainAxisAlignment.start,
227 - mainAxisSize: MainAxisSize.max,
228 - children: [
229 - StandardCheckbox(
230 - value: nodeViewModel.usesEmbeddedProxy,
231 - gradientBackground: false,
232 - borderColor: Theme.of(context).dividerColor,
233 - iconColor: Theme.of(context).colorScheme.primary,
234 - onChanged: null,
235 - caption: 'Embedded Tor SOCKS Proxy',
236 - ),
237 - ],
238 - ),
239 - ),
240 - ],
241 - Padding(
242 - padding: EdgeInsets.only(top: 10),
243 - child: Row(
244 - mainAxisAlignment: MainAxisAlignment.start,
245 - mainAxisSize: MainAxisSize.max,
246 - children: [
247 - StandardCheckbox(
248 - value: nodeViewModel.useSocksProxy,
249 - borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
250 - iconColor: Theme.of(context).colorScheme.primary,
251 - onChanged: (value) {
252 - if (!value) {
253 - _socksAddressController.text = '';
254 - }
255 - nodeViewModel.useSocksProxy = value;
256 - },
257 - caption: 'SOCKS Proxy',
258 - ),
259 - ],
260 - ),
222 + StandardCheckbox(
223 + value: nodeViewModel.usesEmbeddedProxy,
224 + gradientBackground: false,
225 + borderColor: Theme.of(context).dividerColor,
226 + iconColor: Theme.of(context).colorScheme.primary,
227 + onChanged: null,
228 + caption: 'Embedded Tor SOCKS Proxy',
229 ),
262 - if (nodeViewModel.useSocksProxy) ...[
263 - SizedBox(height: 10.0),
264 - Row(
265 - children: <Widget>[
266 - Expanded(
267 - child: BaseTextFormField(
268 - controller: _socksAddressController,
269 - hintText: '[<ip>:]<port>',
270 - validator: SocksProxyNodeAddressValidator(),
271 - ))
272 - ],
273 - ),
274 - ]
230 ],
276 - )),
231 + ),
232 + ),
233 + ],
234 + Padding(
235 + padding: const EdgeInsets.only(top: 10),
236 + child: Row(
237 + mainAxisAlignment: MainAxisAlignment.start,
238 + mainAxisSize: MainAxisSize.max,
239 + children: [
240 + StandardCheckbox(
241 + value: nodeViewModel.useSocksProxy,
242 + borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
243 + iconColor: Theme.of(context).colorScheme.primary,
244 + onChanged: (value) {
245 + if (!value) _socksAddressController.text = '';
246 + nodeViewModel.useSocksProxy = value;
247 + },
248 + caption: 'SOCKS Proxy',
249 + ),
250 + ],
251 + ),
252 + ),
253 + if (nodeViewModel.useSocksProxy) ...[
254 + const SizedBox(height: 10),
255 + Row(children: <Widget>[
256 + Expanded(
257 + child: BaseTextFormField(
258 + controller: _socksAddressController,
259 + hintText: '[<ip>:]<port>',
260 + validator: SocksProxyNodeAddressValidator(),
261 + ),
262 + )
263 + ]),
264 + ]
265 + ]),
266 + ),
267 ]
278 - ],
279 - ),
280 - );
281 - }
268 + ]),
269 + );
270 }