Line data Source code
1 : import 'package:cwtch/cwtch/cwtch.dart';
2 : import 'package:cwtch/cwtch_icons_icons.dart';
3 : import 'package:cwtch/models/servers.dart';
4 : import 'package:cwtch/widgets/cwtchlabel.dart';
5 : import 'package:cwtch/widgets/passwordfield.dart';
6 : import 'package:cwtch/widgets/textfield.dart';
7 : import 'package:flutter/material.dart';
8 : import 'package:cwtch/settings.dart';
9 : import 'package:provider/provider.dart';
10 : import 'package:cwtch/l10n/app_localizations.dart';
11 :
12 : import '../errorHandler.dart';
13 : import '../main.dart';
14 :
15 : /// Pane to add or edit a server
16 : class AddEditServerView extends StatefulWidget {
17 0 : const AddEditServerView();
18 :
19 0 : @override
20 0 : _AddEditServerViewState createState() => _AddEditServerViewState();
21 : }
22 :
23 : class _AddEditServerViewState extends State<AddEditServerView> {
24 : final _formKey = GlobalKey<FormState>();
25 :
26 : final ctrlrDesc = TextEditingController(text: "");
27 : final ctrlrOldPass = TextEditingController(text: "");
28 : final ctrlrPass = TextEditingController(text: "");
29 : final ctrlrPass2 = TextEditingController(text: "");
30 : final ctrlrOnion = TextEditingController(text: "");
31 :
32 : late bool usePassword;
33 :
34 0 : @override
35 : void initState() {
36 0 : super.initState();
37 0 : var serverInfoState = Provider.of<ServerInfoState>(context, listen: false);
38 0 : ctrlrOnion.text = serverInfoState.onion;
39 0 : usePassword = serverInfoState.isEncrypted;
40 0 : if (serverInfoState.description.isNotEmpty) {
41 0 : ctrlrDesc.text = serverInfoState.description;
42 : }
43 : }
44 :
45 0 : @override
46 : void dispose() {
47 0 : super.dispose();
48 : }
49 :
50 0 : @override
51 : Widget build(BuildContext context) {
52 0 : return Scaffold(
53 0 : appBar: AppBar(title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle)),
54 0 : body: _buildSettingsList(),
55 : );
56 : }
57 :
58 0 : void _handleSwitchPassword(bool? value) {
59 0 : setState(() {
60 0 : usePassword = value!;
61 : });
62 : }
63 :
64 0 : Widget _buildSettingsList() {
65 0 : ScrollController controller = ScrollController();
66 0 : return Consumer2<ServerInfoState, Settings>(
67 0 : builder: (context, serverInfoState, settings, child) {
68 0 : return LayoutBuilder(
69 0 : builder: (BuildContext context, BoxConstraints viewportConstraints) {
70 0 : return Scrollbar(
71 : trackVisibility: true,
72 : controller: controller,
73 0 : child: SingleChildScrollView(
74 : controller: controller,
75 : clipBehavior: Clip.antiAlias,
76 0 : child: ConstrainedBox(
77 0 : constraints: BoxConstraints(minHeight: viewportConstraints.maxHeight),
78 0 : child: Form(
79 0 : key: _formKey,
80 0 : child: Container(
81 0 : color: settings.theme.backgroundPaneColor,
82 : //margin: EdgeInsets.fromLTRB(30, 5, 30, 10),
83 0 : padding: EdgeInsets.fromLTRB(50, 10, 50, 20),
84 0 : child: Column(
85 : mainAxisAlignment: MainAxisAlignment.start,
86 : crossAxisAlignment: CrossAxisAlignment.stretch,
87 0 : children: [
88 : // Onion
89 0 : Visibility(
90 0 : visible: serverInfoState.onion.isNotEmpty,
91 0 : child: Column(
92 : mainAxisAlignment: MainAxisAlignment.start,
93 : crossAxisAlignment: CrossAxisAlignment.start,
94 0 : children: [
95 0 : CwtchLabel(label: AppLocalizations.of(context)!.serverAddress),
96 0 : SelectableText(serverInfoState.onion),
97 : ],
98 : ),
99 : ),
100 :
101 : // Description
102 0 : Column(
103 : mainAxisAlignment: MainAxisAlignment.start,
104 : crossAxisAlignment: CrossAxisAlignment.start,
105 0 : children: [
106 0 : SizedBox(height: 20),
107 0 : CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
108 0 : Text(AppLocalizations.of(context)!.serverDescriptionDescription),
109 0 : SizedBox(height: 20),
110 0 : CwtchTextField(controller: ctrlrDesc, hintText: AppLocalizations.of(context)!.fieldDescriptionLabel, autofocus: false),
111 : ],
112 : ),
113 :
114 0 : SizedBox(height: 20),
115 :
116 : // Enabled
117 0 : Visibility(
118 0 : visible: serverInfoState.onion.isNotEmpty,
119 0 : child: SwitchListTile(
120 0 : title: Text(AppLocalizations.of(context)!.serverEnabled, style: TextStyle(color: settings.current().mainTextColor)),
121 0 : subtitle: Text(AppLocalizations.of(context)!.serverEnabledDescription),
122 0 : value: serverInfoState.running,
123 0 : onChanged: (bool value) {
124 0 : serverInfoState.setRunning(value);
125 : if (value) {
126 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchServer(serverInfoState.onion);
127 : } else {
128 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.StopServer(serverInfoState.onion);
129 : }
130 : },
131 0 : activeTrackColor: settings.theme.defaultButtonColor,
132 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
133 0 : secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor),
134 : ),
135 : ),
136 :
137 : // Auto start
138 0 : SwitchListTile(
139 0 : title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor)),
140 0 : subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription),
141 0 : value: serverInfoState.autoStart,
142 0 : onChanged: (bool value) {
143 0 : serverInfoState.setAutostart(value);
144 :
145 0 : if (serverInfoState.onion.isNotEmpty) {
146 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false");
147 : }
148 : },
149 0 : activeTrackColor: settings.theme.defaultButtonColor,
150 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
151 0 : secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor),
152 : ),
153 :
154 : // metrics
155 0 : Visibility(
156 0 : visible: serverInfoState.onion.isNotEmpty && serverInfoState.running,
157 0 : child: Column(
158 : crossAxisAlignment: CrossAxisAlignment.start,
159 0 : children: [
160 0 : SizedBox(height: 20),
161 0 : Text(AppLocalizations.of(context)!.serverMetricsLabel),
162 0 : Row(
163 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
164 0 : children: [
165 0 : Row(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(AppLocalizations.of(context)!.serverTotalMessagesLabel)]),
166 0 : Text(serverInfoState.totalMessages.toString()),
167 : ],
168 : ),
169 0 : Row(
170 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
171 0 : children: [
172 0 : Row(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(AppLocalizations.of(context)!.serverConnectionsLabel)]),
173 0 : Text(serverInfoState.connections.toString()),
174 : ],
175 : ),
176 : ],
177 : ),
178 : ),
179 :
180 : // ***** Password *****
181 :
182 : // use password toggle
183 0 : Visibility(
184 0 : visible: serverInfoState.onion.isEmpty,
185 0 : child: Column(
186 : mainAxisAlignment: MainAxisAlignment.center,
187 0 : children: <Widget>[
188 0 : SizedBox(height: 20),
189 0 : Checkbox(
190 0 : value: usePassword,
191 0 : fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor),
192 0 : activeColor: settings.current().defaultButtonActiveColor,
193 0 : onChanged: _handleSwitchPassword,
194 : ),
195 0 : Text(AppLocalizations.of(context)!.radioUsePassword, style: TextStyle(color: settings.current().mainTextColor)),
196 0 : SizedBox(height: 20),
197 0 : Padding(
198 0 : padding: EdgeInsets.symmetric(horizontal: 24),
199 0 : child: Text(
200 0 : usePassword ? AppLocalizations.of(context)!.encryptedServerDescription : AppLocalizations.of(context)!.plainServerDescription,
201 : textAlign: TextAlign.center,
202 : ),
203 : ),
204 0 : SizedBox(height: 20),
205 : ],
206 : ),
207 : ),
208 :
209 : // current password
210 0 : Visibility(
211 0 : visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted,
212 0 : child: Column(
213 : mainAxisAlignment: MainAxisAlignment.start,
214 : crossAxisAlignment: CrossAxisAlignment.start,
215 0 : children: <Widget>[
216 0 : CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
217 0 : SizedBox(height: 20),
218 0 : CwtchPasswordField(
219 0 : controller: ctrlrOldPass,
220 0 : autoFillHints: [AutofillHints.newPassword],
221 0 : validator: (value) {
222 : // Password field can be empty when just updating the profile, not on creation
223 0 : if (serverInfoState.isEncrypted && serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
224 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
225 : }
226 0 : if (Provider.of<ErrorHandler>(context).deletedServerError == true) {
227 0 : return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer;
228 : }
229 : return null;
230 : },
231 : ),
232 0 : SizedBox(height: 20),
233 : ],
234 : ),
235 : ),
236 :
237 : // new passwords 1 & 2
238 0 : Visibility(
239 : // Currently we don't support password change for servers so also gate this on Add server, when ready to support changing password remove the onion.isEmpty check
240 0 : visible: serverInfoState.onion.isEmpty && usePassword,
241 0 : child: Column(
242 : mainAxisAlignment: MainAxisAlignment.start,
243 : crossAxisAlignment: CrossAxisAlignment.start,
244 0 : children: [
245 0 : CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
246 0 : SizedBox(height: 20),
247 0 : CwtchPasswordField(
248 0 : controller: ctrlrPass,
249 0 : validator: (value) {
250 : // Password field can be empty when just updating the profile, not on creation
251 0 : if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
252 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
253 : }
254 0 : if (value != ctrlrPass2.value.text) {
255 0 : return AppLocalizations.of(context)!.passwordErrorMatch;
256 : }
257 : return null;
258 : },
259 : ),
260 0 : SizedBox(height: 20),
261 0 : CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
262 0 : SizedBox(height: 20),
263 0 : CwtchPasswordField(
264 0 : controller: ctrlrPass2,
265 0 : validator: (value) {
266 : // Password field can be empty when just updating the profile, not on creation
267 0 : if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
268 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
269 : }
270 0 : if (value != ctrlrPass.value.text) {
271 0 : return AppLocalizations.of(context)!.passwordErrorMatch;
272 : }
273 : return null;
274 : },
275 : ),
276 : ],
277 : ),
278 : ),
279 :
280 0 : SizedBox(height: 20),
281 :
282 0 : Column(
283 : mainAxisAlignment: MainAxisAlignment.start,
284 : crossAxisAlignment: CrossAxisAlignment.end,
285 0 : children: [
286 0 : ElevatedButton(
287 0 : onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed,
288 0 : child: Text(serverInfoState.onion.isEmpty ? AppLocalizations.of(context)!.addServerTitle : AppLocalizations.of(context)!.saveServerButton, textAlign: TextAlign.center),
289 : ),
290 : ],
291 : ),
292 0 : Visibility(
293 0 : visible: serverInfoState.onion.isNotEmpty,
294 0 : child: Column(
295 : mainAxisAlignment: MainAxisAlignment.start,
296 : crossAxisAlignment: CrossAxisAlignment.end,
297 0 : children: [
298 0 : SizedBox(height: 20),
299 0 : Tooltip(
300 0 : message: AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer,
301 0 : child: ElevatedButton.icon(
302 0 : onPressed: () {
303 0 : showAlertDialog(context);
304 : },
305 0 : icon: Icon(Icons.delete_forever),
306 0 : label: Text(AppLocalizations.of(context)!.deleteBtn),
307 : ),
308 : ),
309 : ],
310 : ),
311 : ),
312 :
313 : // ***** END Password *****
314 : ],
315 : ),
316 : ),
317 : ),
318 : ),
319 : ),
320 : );
321 : },
322 : );
323 : },
324 : );
325 : }
326 :
327 0 : void _createPressed() {
328 : // This will run all the validations in the form including
329 : // checking that display name is not empty, and an actual check that the passwords
330 : // match (and are provided if the user has requested an encrypted profile).
331 0 : if (_formKey.currentState!.validate()) {
332 0 : if (usePassword) {
333 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
334 : } else {
335 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
336 : }
337 0 : Navigator.of(context).pop();
338 : }
339 : }
340 :
341 0 : void _savePressed() {
342 0 : var server = Provider.of<ServerInfoState>(context, listen: false);
343 :
344 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text);
345 0 : server.setDescription(ctrlrDesc.text);
346 :
347 0 : if (_formKey.currentState!.validate()) {
348 : // TODO support change password
349 : }
350 0 : Navigator.of(context).pop();
351 : }
352 :
353 0 : showAlertDialog(BuildContext context) {
354 : // set up the buttons
355 0 : Widget cancelButton = ElevatedButton(
356 0 : child: Text(AppLocalizations.of(context)!.cancel),
357 0 : onPressed: () {
358 0 : Navigator.of(context).pop(); // dismiss dialog
359 : },
360 : );
361 : // For #939: provide an alert that a password must be provided to delete a password-protected profile.
362 0 : Widget errorAcknowledgeButton = ElevatedButton(
363 0 : child: Text(AppLocalizations.of(context)!.cancel),
364 0 : onPressed: () {
365 0 : Navigator.of(context)
366 0 : ..pop()
367 0 : ..pop(); // dismiss dialog
368 : },
369 : );
370 0 : AlertDialog passwordRequiredToDeleteServerAlert = AlertDialog(title: Text(AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer), actions: [errorAcknowledgeButton]);
371 0 : Widget continueButton = ElevatedButton(
372 0 : child: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn),
373 0 : onPressed: () {
374 0 : var onion = Provider.of<ServerInfoState>(context, listen: false).onion;
375 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteServer(onion, Provider.of<ServerInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword);
376 0 : Future.delayed(const Duration(milliseconds: 500), () {
377 0 : if (globalErrorHandler.deletedServerSuccess) {
378 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteServerSuccess + ":" + onion));
379 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
380 0 : Navigator.of(context).popUntil((route) => route.settings.name == "servers"); // dismiss dialog
381 : } else {
382 0 : showDialog(
383 : context: context,
384 0 : builder: (BuildContext context) {
385 : return passwordRequiredToDeleteServerAlert;
386 : },
387 : );
388 : }
389 : });
390 : },
391 : );
392 : // set up the AlertDialog
393 0 : AlertDialog alert = AlertDialog(title: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn), actions: [cancelButton, continueButton]);
394 :
395 : // show the dialog
396 0 : showDialog(
397 : context: context,
398 0 : builder: (BuildContext context) {
399 : return alert;
400 : },
401 : );
402 : }
403 : }
|