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