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 : onChanged: (newVal) {
133 0 : setState(() {
134 0 : this.selectedGroupAdmin = newVal;
135 : });
136 : },
137 : ),
138 : ),
139 : ],
140 : ),
141 : ),
142 :
143 0 : SizedBox(height: 20),
144 :
145 : // Enabled
146 0 : Visibility(
147 0 : visible: hybridGroupState.onion.isNotEmpty,
148 0 : child: SwitchListTile(
149 0 : title: Text(AppLocalizations.of(context)!.hybridGroupEnabled, style: TextStyle(color: settings.current().mainTextColor)),
150 0 : subtitle: Text(AppLocalizations.of(context)!.hybridGroupEnabledDescription),
151 0 : value: hybridGroupState.running,
152 0 : onChanged: (bool value) {
153 0 : hybridGroupState.setRunning(value);
154 : if (value) {
155 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchHybridGroup(hybridGroupState.onion);
156 : } else {
157 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.StopHybridGroup(hybridGroupState.onion);
158 : }
159 : },
160 0 : activeTrackColor: settings.theme.defaultButtonColor,
161 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
162 0 : secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor),
163 : ),
164 : ),
165 :
166 : // Auto start
167 0 : SwitchListTile(
168 0 : title: Text(AppLocalizations.of(context)!.hybridGroupAutostartLabel, style: TextStyle(color: settings.current().mainTextColor)),
169 0 : subtitle: Text(AppLocalizations.of(context)!.hybridGroupAutostartDescription),
170 0 : value: hybridGroupState.autoStart,
171 0 : onChanged: (bool value) {
172 0 : hybridGroupState.setAutostart(value);
173 :
174 0 : if (hybridGroupState.onion.isNotEmpty) {
175 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetHybridGroupAttribute(hybridGroupState.onion, "autostart", value ? "true" : "false");
176 : }
177 : },
178 0 : activeTrackColor: settings.theme.defaultButtonColor,
179 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
180 0 : secondary: Icon(CwtchIcons.favorite_24px, color: settings.current().mainTextColor),
181 : ),
182 :
183 : // metrics
184 0 : Visibility(
185 0 : visible: hybridGroupState.onion.isNotEmpty && hybridGroupState.running,
186 0 : child: Column(
187 : crossAxisAlignment: CrossAxisAlignment.start,
188 0 : children: [
189 0 : SizedBox(height: 20),
190 0 : Text(AppLocalizations.of(context)!.hybridGroupMetricsLabel),
191 0 : Row(
192 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
193 0 : children: [
194 0 : Row(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(AppLocalizations.of(context)!.hybridGroupTotalMessagesLabel)]),
195 0 : Text(hybridGroupState.totalMessages.toString()),
196 : ],
197 : ),
198 0 : Row(
199 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
200 0 : children: [
201 0 : Row(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(AppLocalizations.of(context)!.hybridGroupConnectionsLabel)]),
202 0 : Text(hybridGroupState.connections.toString()),
203 : ],
204 : ),
205 : ],
206 : ),
207 : ),
208 :
209 : // ***** Password *****
210 :
211 : // use password toggle
212 0 : Visibility(
213 0 : visible: hybridGroupState.onion.isEmpty,
214 0 : child: Column(
215 : mainAxisAlignment: MainAxisAlignment.center,
216 0 : children: <Widget>[
217 0 : SizedBox(height: 20),
218 0 : Checkbox(
219 0 : value: usePassword,
220 0 : fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor),
221 0 : activeColor: settings.current().defaultButtonActiveColor,
222 0 : onChanged: _handleSwitchPassword,
223 : ),
224 0 : Text(AppLocalizations.of(context)!.radioUsePassword, style: TextStyle(color: settings.current().mainTextColor)),
225 0 : SizedBox(height: 20),
226 0 : Padding(
227 0 : padding: EdgeInsets.symmetric(horizontal: 24),
228 0 : child: Text(
229 0 : usePassword ? AppLocalizations.of(context)!.encryptedHybridGroupDescription : AppLocalizations.of(context)!.plainHybridGroupDescription,
230 : textAlign: TextAlign.center,
231 : ),
232 : ),
233 0 : SizedBox(height: 20),
234 : ],
235 : ),
236 : ),
237 :
238 : // current password
239 0 : Visibility(
240 0 : visible: hybridGroupState.onion.isNotEmpty && hybridGroupState.isEncrypted,
241 0 : child: Column(
242 : mainAxisAlignment: MainAxisAlignment.start,
243 : crossAxisAlignment: CrossAxisAlignment.start,
244 0 : children: <Widget>[
245 0 : CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
246 0 : SizedBox(height: 20),
247 0 : CwtchPasswordField(
248 0 : controller: ctrlrOldPass,
249 0 : autoFillHints: [AutofillHints.newPassword],
250 0 : validator: (value) {
251 : // Password field can be empty when just updating the profile, not on creation
252 0 : if (hybridGroupState.isEncrypted && hybridGroupState.onion.isEmpty && value.isEmpty && usePassword) {
253 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
254 : }
255 0 : if (Provider.of<ErrorHandler>(context).deletedHybridGroupError == true) {
256 0 : return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteHybridGroup;
257 : }
258 : return null;
259 : },
260 : ),
261 0 : SizedBox(height: 20),
262 : ],
263 : ),
264 : ),
265 :
266 : // new passwords 1 & 2
267 0 : Visibility(
268 : // 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
269 0 : visible: hybridGroupState.onion.isEmpty && usePassword,
270 0 : child: Column(
271 : mainAxisAlignment: MainAxisAlignment.start,
272 : crossAxisAlignment: CrossAxisAlignment.start,
273 0 : children: [
274 0 : CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
275 0 : SizedBox(height: 20),
276 0 : CwtchPasswordField(
277 0 : controller: ctrlrPass,
278 0 : validator: (value) {
279 : // Password field can be empty when just updating the profile, not on creation
280 0 : if (hybridGroupState.onion.isEmpty && value.isEmpty && usePassword) {
281 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
282 : }
283 0 : if (value != ctrlrPass2.value.text) {
284 0 : return AppLocalizations.of(context)!.passwordErrorMatch;
285 : }
286 : return null;
287 : },
288 : ),
289 0 : SizedBox(height: 20),
290 0 : CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
291 0 : SizedBox(height: 20),
292 0 : CwtchPasswordField(
293 0 : controller: ctrlrPass2,
294 0 : validator: (value) {
295 : // Password field can be empty when just updating the profile, not on creation
296 0 : if (hybridGroupState.onion.isEmpty && value.isEmpty && usePassword) {
297 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
298 : }
299 0 : if (value != ctrlrPass.value.text) {
300 0 : return AppLocalizations.of(context)!.passwordErrorMatch;
301 : }
302 : return null;
303 : },
304 : ),
305 : ],
306 : ),
307 : ),
308 :
309 0 : SizedBox(height: 20),
310 :
311 0 : Column(
312 : mainAxisAlignment: MainAxisAlignment.start,
313 : crossAxisAlignment: CrossAxisAlignment.end,
314 0 : children: [
315 0 : ElevatedButton(
316 0 : onPressed: this.selectedGroupAdmin.isEmpty ? null : (hybridGroupState.onion.isEmpty ? _createPressed : _savePressed),
317 0 : child: Text(
318 0 : hybridGroupState.onion.isEmpty ? AppLocalizations.of(context)!.addHybridGroupTitle : AppLocalizations.of(context)!.saveHybridGroupButton,
319 : textAlign: TextAlign.center,
320 : ),
321 : ),
322 : ],
323 : ),
324 0 : Visibility(
325 0 : visible: hybridGroupState.onion.isNotEmpty,
326 0 : child: Column(
327 : mainAxisAlignment: MainAxisAlignment.start,
328 : crossAxisAlignment: CrossAxisAlignment.end,
329 0 : children: [
330 0 : SizedBox(height: 20),
331 0 : Tooltip(
332 0 : message: AppLocalizations.of(context)!.enterCurrentPasswordForDeleteHybridGroup,
333 0 : child: ElevatedButton.icon(
334 0 : onPressed: () {
335 0 : showAlertDialog(context);
336 : },
337 0 : icon: Icon(Icons.delete_forever),
338 0 : label: Text(AppLocalizations.of(context)!.deleteBtn),
339 : ),
340 : ),
341 : ],
342 : ),
343 : ),
344 :
345 : // ***** END Password *****
346 : ],
347 : ),
348 : ),
349 : ),
350 : ),
351 : ),
352 : );
353 : },
354 : );
355 : },
356 : );
357 : }
358 :
359 0 : void _createPressed() {
360 : // This will run all the validations in the form including
361 : // checking that display name is not empty, and an actual check that the passwords
362 : // match (and are provided if the user has requested an encrypted profile).
363 0 : if (_formKey.currentState!.validate()) {
364 0 : if (usePassword) {
365 0 : Provider.of<FlwtchState>(
366 0 : context,
367 : listen: false,
368 0 : ).cwtch.CreateHybridGroup(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<HybridGroupState>(context, listen: false).autoStart, this.selectedGroupAdmin);
369 : } else {
370 0 : Provider.of<FlwtchState>(
371 0 : context,
372 : listen: false,
373 0 : ).cwtch.CreateHybridGroup(DefaultPassword, ctrlrDesc.value.text, Provider.of<HybridGroupState>(context, listen: false).autoStart, this.selectedGroupAdmin);
374 : }
375 0 : Navigator.of(context).pop();
376 : }
377 : }
378 :
379 0 : void _savePressed() {
380 0 : var group = Provider.of<HybridGroupState>(context, listen: false);
381 :
382 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetHybridGroupAttribute(group.onion, "description", ctrlrDesc.text);
383 0 : group.setDescription(ctrlrDesc.text);
384 :
385 0 : if (_formKey.currentState!.validate()) {
386 : // TODO support change password
387 : }
388 0 : Navigator.of(context).pop();
389 : }
390 :
391 0 : showAlertDialog(BuildContext context) {
392 : // set up the buttons
393 0 : Widget cancelButton = ElevatedButton(
394 0 : child: Text(AppLocalizations.of(context)!.cancel),
395 0 : onPressed: () {
396 0 : Navigator.of(context).pop(); // dismiss dialog
397 : },
398 : );
399 : // For #939: provide an alert that a password must be provided to delete a password-protected profile.
400 0 : Widget errorAcknowledgeButton = ElevatedButton(
401 0 : child: Text(AppLocalizations.of(context)!.cancel),
402 0 : onPressed: () {
403 0 : Navigator.of(context)
404 0 : ..pop()
405 0 : ..pop(); // dismiss dialog
406 : },
407 : );
408 0 : AlertDialog passwordRequiredToDeleteHybridGroupAlert = AlertDialog(title: Text(AppLocalizations.of(context)!.enterCurrentPasswordForDeleteHybridGroup), actions: [errorAcknowledgeButton]);
409 0 : Widget continueButton = ElevatedButton(
410 0 : child: Text(AppLocalizations.of(context)!.deleteHybridGroupConfirmBtn),
411 0 : onPressed: () {
412 0 : var onion = Provider.of<HybridGroupState>(context, listen: false).onion;
413 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteHybridGroup(onion, Provider.of<HybridGroupState>(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword);
414 0 : Future.delayed(const Duration(milliseconds: 500), () {
415 0 : if (globalErrorHandler.deletedHybridGroupSuccess) {
416 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteHybridGroupSuccess + ":" + onion));
417 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
418 0 : Navigator.of(context).popUntil((route) => route.settings.name == "hybridgroups"); // dismiss dialog
419 : } else {
420 0 : showDialog(
421 : context: context,
422 0 : builder: (BuildContext context) {
423 : return passwordRequiredToDeleteHybridGroupAlert;
424 : },
425 : );
426 : }
427 : });
428 : },
429 : );
430 : // set up the AlertDialog
431 0 : AlertDialog alert = AlertDialog(title: Text(AppLocalizations.of(context)!.deleteHybridGroupConfirmBtn), actions: [cancelButton, continueButton]);
432 :
433 : // show the dialog
434 0 : showDialog(
435 : context: context,
436 0 : builder: (BuildContext context) {
437 : return alert;
438 : },
439 : );
440 : }
441 : }
|