Line data Source code
1 : import 'dart:io';
2 :
3 : import 'package:cwtch/config.dart';
4 : import 'package:cwtch/controllers/filesharing.dart';
5 : import 'package:cwtch/cwtch/cwtch.dart';
6 : import 'package:cwtch/models/appstate.dart';
7 : import 'package:cwtch/models/profile.dart';
8 : import 'package:cwtch/controllers/filesharing.dart' as filesharing;
9 : import 'package:flutter/material.dart';
10 : import 'package:flutter/services.dart';
11 : import 'package:cwtch/widgets/buttontextfield.dart';
12 : import 'package:cwtch/widgets/cwtchlabel.dart';
13 : import 'package:cwtch/widgets/passwordfield.dart';
14 : import 'package:cwtch/widgets/profileimage.dart';
15 : import 'package:cwtch/widgets/textfield.dart';
16 : import 'package:provider/provider.dart';
17 : import 'package:cwtch/l10n/app_localizations.dart';
18 :
19 : import '../constants.dart';
20 : import '../cwtch_icons_icons.dart';
21 : import '../errorHandler.dart';
22 : import '../main.dart';
23 : import '../settings.dart';
24 :
25 : class AddEditProfileView extends StatefulWidget {
26 0 : const AddEditProfileView({Key? key}) : super(key: key);
27 :
28 0 : @override
29 0 : _AddEditProfileViewState createState() => _AddEditProfileViewState();
30 : }
31 :
32 : class _AddEditProfileViewState extends State<AddEditProfileView> {
33 : final _formKey = GlobalKey<FormState>();
34 :
35 : final ctrlrNick = TextEditingController(text: "");
36 : final ctrlrPrivateName = TextEditingController(text: "");
37 : final ctrlrOldPass = TextEditingController(text: "");
38 : final ctrlrPass = TextEditingController(text: "");
39 : final ctrlrPass2 = TextEditingController(text: "");
40 : final ctrlrOnion = TextEditingController(text: "");
41 :
42 : final ctrlrAttribute1 = TextEditingController(text: "");
43 : final ctrlrAttribute2 = TextEditingController(text: "");
44 : final ctrlrAttribute3 = TextEditingController(text: "");
45 :
46 : ScrollController controller = ScrollController();
47 : late bool usePassword;
48 : late bool deleted;
49 :
50 0 : @override
51 : void initState() {
52 0 : super.initState();
53 0 : usePassword = true;
54 0 : final nickname = Provider.of<ProfileInfoState>(context, listen: false).nickname;
55 0 : if (nickname.isNotEmpty) {
56 0 : ctrlrNick.text = nickname;
57 : }
58 0 : ctrlrPrivateName.text = Provider.of<ProfileInfoState>(context, listen: false).getPrivateName();
59 : }
60 :
61 0 : @override
62 : Widget build(BuildContext context) {
63 0 : ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion;
64 0 : return Scaffold(
65 0 : appBar: AppBar(title: Text(Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addProfileTitle : AppLocalizations.of(context)!.editProfileTitle)),
66 0 : body: _buildForm(),
67 : );
68 : }
69 :
70 0 : void _handleSwitchPassword(bool? value) {
71 0 : setState(() {
72 0 : usePassword = value!;
73 : });
74 : }
75 :
76 : // A few implementation notes
77 : // We use Visibility to hide optional structures when they are not requested.
78 : // We used SizedBox for inter-widget height padding in columns, otherwise elements can render a little too close together.
79 0 : Widget _buildForm() {
80 0 : return Consumer<Settings>(
81 0 : builder: (context, theme, child) {
82 0 : return LayoutBuilder(
83 0 : builder: (BuildContext context, BoxConstraints viewportConstraints) {
84 0 : return Scrollbar(
85 : trackVisibility: true,
86 0 : controller: controller,
87 0 : child: SingleChildScrollView(
88 0 : controller: controller,
89 : clipBehavior: Clip.antiAlias,
90 0 : child: ConstrainedBox(
91 0 : constraints: BoxConstraints(minHeight: viewportConstraints.maxHeight),
92 0 : child: Form(
93 0 : key: _formKey,
94 0 : child: Material(
95 0 : color: theme.theme.backgroundPaneColor,
96 0 : child: Padding(
97 0 : padding: EdgeInsets.all(50),
98 0 : child: Column(
99 : mainAxisAlignment: MainAxisAlignment.start,
100 : crossAxisAlignment: CrossAxisAlignment.center,
101 0 : children: [
102 0 : Visibility(
103 0 : visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty,
104 0 : child: Column(
105 : crossAxisAlignment: CrossAxisAlignment.center,
106 : mainAxisAlignment: MainAxisAlignment.center,
107 : mainAxisSize: MainAxisSize.min,
108 0 : children: [
109 0 : Row(
110 : mainAxisAlignment: MainAxisAlignment.center,
111 0 : children: <Widget>[
112 0 : MouseRegion(
113 0 : cursor: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? SystemMouseCursors.click : SystemMouseCursors.basic,
114 0 : child: GestureDetector(
115 : // don't allow setting of profile images if the image previews experiment is disabled.
116 : onTap:
117 0 : Provider.of<AppState>(context, listen: false).disableFilePicker ||
118 0 : !Provider.of<Settings>(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment)
119 : ? null
120 0 : : () {
121 0 : filesharing.showFilePicker(
122 : context,
123 : MaxImageFileSharingSize,
124 0 : (File file) {
125 0 : var profile = Provider.of<ProfileInfoState>(context, listen: false).onion;
126 : // Share this image publicly (conversation handle == -1)
127 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ShareFile(profile, -1, file.path);
128 : // update the image cache locally
129 0 : Provider.of<ProfileInfoState>(context, listen: false).imagePath = file.path;
130 : },
131 0 : () {
132 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.msgFileTooBig), duration: Duration(seconds: 4));
133 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
134 : },
135 0 : () {},
136 : );
137 : },
138 0 : child: ProfileImage(
139 0 : imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
140 0 : ? Provider.of<ProfileInfoState>(context).imagePath
141 0 : : Provider.of<ProfileInfoState>(context).defaultImagePath,
142 : diameter: 120,
143 0 : tooltip: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage : "",
144 : maskOut: false,
145 0 : border: theme.theme.portraitOnlineBorderColor,
146 0 : badgeTextColor: theme.theme.portraitContactBadgeTextColor,
147 0 : badgeColor: theme.theme.portraitContactBadgeColor,
148 0 : badgeEdit: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment),
149 : ),
150 : ),
151 : ),
152 : ],
153 : ),
154 0 : SizedBox(
155 0 : width: MediaQuery.of(context).size.width / 2,
156 0 : child: Column(
157 0 : children: [
158 0 : Padding(
159 0 : padding: EdgeInsets.all(5.0),
160 0 : child: CwtchTextField(
161 0 : controller: ctrlrAttribute1,
162 : multiLine: false,
163 0 : onChanged: (profileAttribute1) {
164 0 : String onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
165 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-attribute-1", profileAttribute1);
166 0 : Provider.of<ProfileInfoState>(context, listen: false).attributes[0] = profileAttribute1;
167 : },
168 0 : hintText: Provider.of<ProfileInfoState>(context).attributes[0] ?? AppLocalizations.of(context)!.profileInfoHint,
169 : ),
170 : ),
171 0 : Padding(
172 0 : padding: EdgeInsets.all(5.0),
173 0 : child: CwtchTextField(
174 0 : controller: ctrlrAttribute2,
175 : multiLine: false,
176 0 : onChanged: (profileAttribute2) {
177 0 : String onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
178 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-attribute-2", profileAttribute2);
179 0 : Provider.of<ProfileInfoState>(context, listen: false).attributes[1] = profileAttribute2;
180 : },
181 0 : hintText: Provider.of<ProfileInfoState>(context).attributes[1] ?? AppLocalizations.of(context)!.profileInfoHint2,
182 : ),
183 : ),
184 0 : Padding(
185 0 : padding: EdgeInsets.all(5.0),
186 0 : child: CwtchTextField(
187 0 : controller: ctrlrAttribute3,
188 : multiLine: false,
189 0 : onChanged: (profileAttribute3) {
190 0 : String onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
191 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-attribute-3", profileAttribute3);
192 0 : Provider.of<ProfileInfoState>(context, listen: false).attributes[2] = profileAttribute3;
193 : },
194 0 : hintText: Provider.of<ProfileInfoState>(context).attributes[2] ?? AppLocalizations.of(context)!.profileInfoHint3,
195 : ),
196 : ),
197 : ],
198 : ),
199 : ),
200 : ],
201 : ),
202 : ),
203 0 : Column(
204 : crossAxisAlignment: CrossAxisAlignment.start,
205 0 : children: [
206 0 : SizedBox(height: 20),
207 0 : CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
208 0 : SizedBox(height: 20),
209 0 : CwtchTextField(
210 0 : key: Key("displayNameFormElement"),
211 0 : controller: ctrlrNick,
212 : autofocus: false,
213 0 : hintText: AppLocalizations.of(context)!.yourDisplayName,
214 0 : validator: (value) {
215 0 : if (value.isEmpty) {
216 0 : return AppLocalizations.of(context)!.displayNameTooltip;
217 : }
218 : return null;
219 : },
220 : ),
221 0 : SizedBox(height: 20),
222 0 : CwtchLabel(label: AppLocalizations.of(context)!.privateNameLabel),
223 0 : SizedBox(height: 20),
224 0 : CwtchTextField(key: Key("privateNameFormElement"), controller: ctrlrPrivateName, autofocus: false, hintText: AppLocalizations.of(context)!.privateNameHint),
225 : ],
226 : ),
227 0 : Visibility(
228 0 : visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty,
229 0 : child: Column(
230 : mainAxisAlignment: MainAxisAlignment.start,
231 : crossAxisAlignment: CrossAxisAlignment.start,
232 0 : children: [
233 0 : SizedBox(height: 20),
234 0 : CwtchLabel(label: AppLocalizations.of(context)!.addressLabel),
235 0 : SizedBox(height: 20),
236 0 : CwtchButtonTextField(
237 0 : controller: ctrlrOnion,
238 0 : onPressed: _copyOnion,
239 : readonly: true,
240 0 : icon: Icon(CwtchIcons.address_copy, size: 32),
241 0 : tooltip: AppLocalizations.of(context)!.copyBtn,
242 : ),
243 : ],
244 : ),
245 : ),
246 : // We only allow setting password types on profile creation
247 :
248 : // Enabled
249 0 : Visibility(
250 : // FIXME don't show the disable switch in test mode...this is a bug relating to scrolling things into view
251 0 : visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty && (EnvironmentConfig.TEST_MODE == false),
252 0 : child: SwitchListTile(
253 0 : title: Text(AppLocalizations.of(context)!.profileEnabled, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor)),
254 0 : subtitle: Text(AppLocalizations.of(context)!.profileEnabledDescription),
255 0 : value: Provider.of<ProfileInfoState>(context).enabled,
256 0 : onChanged: (bool value) {
257 0 : Provider.of<ProfileInfoState>(context, listen: false).enabled = value;
258 : if (value) {
259 0 : if (Provider.of<ProfileInfoState>(context, listen: false).appearOffline == false) {
260 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(Provider.of<ProfileInfoState>(context, listen: false).onion, true, true, true);
261 : } else {
262 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(Provider.of<ProfileInfoState>(context, listen: false).onion, false, false, false);
263 : }
264 : } else {
265 0 : Provider.of<ProfileInfoState>(context, listen: false).deactivatePeerEngine(context);
266 : }
267 : },
268 0 : activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
269 0 : inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
270 0 : secondary: Icon(CwtchIcons.negative_heart_24px, color: Provider.of<Settings>(context).current().mainTextColor),
271 : ),
272 : ),
273 :
274 : // Auto start
275 0 : SwitchListTile(
276 0 : title: Text(AppLocalizations.of(context)!.profileAutostartLabel, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor)),
277 0 : subtitle: Text(AppLocalizations.of(context)!.profileAutostartDescription),
278 0 : value: Provider.of<ProfileInfoState>(context).autostart,
279 0 : onChanged: (bool value) {
280 0 : Provider.of<ProfileInfoState>(context, listen: false).autostart = value;
281 :
282 0 : if (Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty) {
283 0 : Provider.of<FlwtchState>(
284 : context,
285 : listen: false,
286 0 : ).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.autostart", value ? "true" : "false");
287 : }
288 : },
289 0 : activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
290 0 : inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
291 0 : secondary: Icon(CwtchIcons.favorite_24dp, color: Provider.of<Settings>(context).current().mainTextColor),
292 : ),
293 :
294 : // Appear Offline
295 0 : Visibility(
296 : // FIXME don't show the disable switch in test mode...this is a bug relating to scrolling things into view
297 0 : visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty && (EnvironmentConfig.TEST_MODE == false),
298 0 : child: SwitchListTile(
299 0 : title: Text(AppLocalizations.of(context)!.profileOfflineAtStart, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor)),
300 0 : subtitle: Text(AppLocalizations.of(context)!.profileAppearOfflineDescription),
301 0 : value: Provider.of<ProfileInfoState>(context).appearOfflineAtStartup,
302 0 : onChanged: (bool value) {
303 0 : Provider.of<ProfileInfoState>(context, listen: false).appearOfflineAtStartup = value;
304 0 : var onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
305 0 : if (onion.isNotEmpty) {
306 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.appear-offline", value ? "true" : "false");
307 : }
308 : },
309 0 : activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
310 0 : inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
311 0 : secondary: Icon(CwtchIcons.favorite_24dp, color: Provider.of<Settings>(context).current().mainTextColor),
312 : ),
313 : ),
314 :
315 0 : Visibility(visible: Provider.of<ProfileInfoState>(context).onion.isEmpty, child: SizedBox(height: 20)),
316 0 : Visibility(
317 0 : visible: Provider.of<ProfileInfoState>(context).onion.isEmpty,
318 0 : child: Column(
319 : mainAxisAlignment: MainAxisAlignment.center,
320 0 : children: <Widget>[
321 0 : Checkbox(
322 0 : key: Key("passwordCheckBox"),
323 0 : value: usePassword,
324 0 : fillColor: MaterialStateProperty.all(theme.current().defaultButtonColor),
325 0 : activeColor: theme.current().defaultButtonActiveColor,
326 0 : onChanged: _handleSwitchPassword,
327 : ),
328 0 : Text(AppLocalizations.of(context)!.radioUsePassword, style: TextStyle(color: theme.current().mainTextColor)),
329 0 : SizedBox(height: 20),
330 0 : Padding(
331 0 : padding: EdgeInsets.symmetric(horizontal: 24),
332 0 : child: Text(
333 0 : usePassword ? AppLocalizations.of(context)!.encryptedProfileDescription : AppLocalizations.of(context)!.plainProfileDescription,
334 : textAlign: TextAlign.center,
335 : ),
336 : ),
337 : ],
338 : ),
339 : ),
340 0 : SizedBox(height: 20),
341 0 : Visibility(
342 0 : visible: usePassword,
343 0 : child: Column(
344 : mainAxisAlignment: MainAxisAlignment.start,
345 : crossAxisAlignment: CrossAxisAlignment.start,
346 0 : children: <Widget>[
347 0 : Visibility(
348 0 : visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty && Provider.of<ProfileInfoState>(context).isEncrypted,
349 0 : child: Column(
350 : mainAxisAlignment: MainAxisAlignment.start,
351 : crossAxisAlignment: CrossAxisAlignment.start,
352 0 : children: [
353 0 : CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
354 0 : SizedBox(height: 20),
355 0 : CwtchPasswordField(
356 0 : key: Key("currentPasswordFormElement"),
357 0 : controller: ctrlrOldPass,
358 0 : autoFillHints: [AutofillHints.newPassword],
359 0 : validator: (value) {
360 : // Password field can be empty when just updating the profile, not on creation
361 0 : if (Provider.of<ProfileInfoState>(context, listen: false).isEncrypted &&
362 0 : Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty &&
363 0 : value.isEmpty &&
364 0 : usePassword) {
365 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
366 : }
367 0 : if (Provider.of<ErrorHandler>(context, listen: false).deleteProfileError == true) {
368 0 : return AppLocalizations.of(context)!.enterCurrentPasswordForDelete;
369 : }
370 : return null;
371 : },
372 : ),
373 0 : SizedBox(height: 20),
374 : ],
375 : ),
376 : ),
377 0 : CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
378 0 : SizedBox(height: 20),
379 0 : CwtchPasswordField(
380 0 : key: Key("passwordFormElement"),
381 0 : controller: ctrlrPass,
382 0 : validator: (value) {
383 : // Password field can be empty when just updating the profile, not on creation
384 0 : if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
385 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
386 : }
387 0 : if (value != ctrlrPass2.value.text) {
388 0 : return AppLocalizations.of(context)!.passwordErrorMatch;
389 : }
390 : return null;
391 : },
392 : ),
393 0 : SizedBox(height: 20),
394 0 : CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
395 0 : SizedBox(height: 20),
396 0 : CwtchPasswordField(
397 0 : key: Key("confirmPasswordFormElement"),
398 0 : controller: ctrlrPass2,
399 0 : validator: (value) {
400 : // Password field can be empty when just updating the profile, not on creation
401 0 : if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
402 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
403 : }
404 0 : if (value != ctrlrPass.value.text) {
405 0 : return AppLocalizations.of(context)!.passwordErrorMatch;
406 : }
407 : return null;
408 : },
409 : ),
410 : ],
411 : ),
412 : ),
413 0 : SizedBox(height: 20),
414 0 : ElevatedButton(
415 0 : key: Key("createOrSaveProfileBtn"),
416 0 : onPressed: _createPressed,
417 0 : style: ElevatedButton.styleFrom(
418 0 : minimumSize: Size(400, 75),
419 0 : maximumSize: Size(800, 75),
420 0 : shape: RoundedRectangleBorder(
421 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
422 : ),
423 : ),
424 0 : child: Text(
425 0 : Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn,
426 : textAlign: TextAlign.center,
427 : ),
428 : ),
429 0 : SizedBox(height: 20),
430 0 : Visibility(
431 0 : visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
432 0 : child: Tooltip(
433 0 : message: AppLocalizations.of(context)!.exportProfileTooltip,
434 0 : child: OutlinedButton.icon(
435 0 : style: OutlinedButton.styleFrom(minimumSize: Size(400, 75), maximumSize: Size(800, 75)),
436 0 : onPressed: () {
437 0 : if (Platform.isAndroid) {
438 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, ctrlrOnion.value.text + ".tar.gz");
439 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + ctrlrOnion.value.text + ".tar.gz"));
440 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
441 : } else {
442 0 : showCreateFilePicker(context).then((name) {
443 : if (name != null) {
444 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, name);
445 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + name));
446 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
447 : }
448 : });
449 : }
450 : },
451 0 : icon: Icon(Icons.import_export),
452 0 : label: Text(AppLocalizations.of(context)!.exportProfile),
453 : ),
454 : ),
455 : ),
456 0 : SizedBox(height: 20),
457 0 : Visibility(
458 0 : visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
459 0 : child: Tooltip(
460 0 : message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
461 0 : child: FilledButton.icon(
462 0 : style: FilledButton.styleFrom(
463 0 : minimumSize: Size(400, 75),
464 0 : maximumSize: Size(800, 75),
465 0 : shape: RoundedRectangleBorder(
466 0 : side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
467 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
468 : ),
469 : ),
470 0 : onPressed: () {
471 0 : showAlertDialog(context);
472 : },
473 0 : icon: Icon(Icons.delete_forever),
474 0 : label: Text(AppLocalizations.of(context)!.deleteBtn),
475 : ),
476 : ),
477 : ),
478 : ],
479 : ),
480 : ),
481 : ),
482 : ),
483 : ),
484 : ),
485 : );
486 : },
487 : );
488 : },
489 : );
490 : }
491 :
492 0 : void _copyOnion() {
493 0 : Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
494 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
495 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
496 : }
497 :
498 0 : void _createPressed() async {
499 : // This will run all the validations in the form including
500 : // checking that display name is not empty, and an actual check that the passwords
501 : // match (and are provided if the user has requested an encrypted profile).
502 0 : if (_formKey.currentState!.validate()) {
503 0 : if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty) {
504 0 : if (usePassword == true) {
505 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text, Provider.of<ProfileInfoState>(context, listen: false).autostart).then((
506 : profile_id,
507 : ) {
508 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile_id, "profile.private-name", ctrlrPrivateName.value.text);
509 : });
510 : } else {
511 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, DefaultPassword, Provider.of<ProfileInfoState>(context, listen: false).autostart).then((
512 : profile_id,
513 : ) {
514 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile_id, "profile.private-name", ctrlrPrivateName.value.text);
515 : });
516 : }
517 0 : Navigator.of(context).pop();
518 : } else {
519 : // Profile Editing
520 0 : var profile = Provider.of<ProfileInfoState>(context, listen: false).onion;
521 : // update name
522 0 : Provider.of<ProfileInfoState>(context, listen: false).nickname = ctrlrNick.value.text;
523 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile, "profile.name", ctrlrNick.value.text);
524 0 : Provider.of<ProfileInfoState>(context, listen: false).setPrivateName(ctrlrPrivateName.value.text);
525 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile, "profile.private-name", ctrlrPrivateName.value.text);
526 :
527 0 : if (ctrlrPass.value.text.isEmpty) {
528 0 : Navigator.of(context).pop();
529 : } else {
530 : // At this points passwords have been validated to be the same and not empty so update password
531 : // Use default password if the profile is unencrypted
532 0 : var password = Provider.of<ProfileInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.text : DefaultPassword;
533 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ChangePassword(profile, password, ctrlrPass.text, ctrlrPass2.text);
534 :
535 0 : EnvironmentConfig.debugLog("waiting for change password response");
536 0 : Future.delayed(const Duration(milliseconds: 500), () {
537 0 : if (globalErrorHandler.changePasswordError) {
538 : // TODO: This isn't ideal, but because onChange can be fired during this future check
539 : // and because the context can change after being popped we have this kind of double assertion...
540 : // There is probably a better pattern to handle this...
541 0 : if (AppLocalizations.of(context) != null) {
542 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.passwordChangeError));
543 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
544 : return;
545 : }
546 : }
547 0 : }).whenComplete(() {
548 0 : if (globalErrorHandler.explicitChangePasswordSuccess) {
549 : // we need to set the local encrypted status to display correct password forms on this run...
550 0 : Provider.of<ProfileInfoState>(context, listen: false).isEncrypted = true;
551 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.newPassword));
552 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
553 0 : Navigator.pop(context);
554 : return; // otherwise round and round we go...
555 : }
556 : });
557 : }
558 : }
559 : }
560 : }
561 :
562 0 : showAlertDialog(BuildContext context) {
563 : // set up the buttons
564 0 : Widget cancelButton = ElevatedButton(
565 0 : child: Text(AppLocalizations.of(context)!.cancel),
566 0 : onPressed: () {
567 0 : Navigator.of(context).pop(); // dismiss dialog
568 : },
569 : );
570 : // For #939: provide an alert that a password must be provided to delete a password-protected profile.
571 0 : Widget errorAcknowledgeButton = ElevatedButton(
572 0 : child: Text(AppLocalizations.of(context)!.cancel),
573 0 : onPressed: () {
574 0 : Navigator.of(context)
575 0 : ..pop()
576 0 : ..pop(); // dismiss dialog
577 : },
578 : );
579 0 : AlertDialog passwordRequiredToDeleteAlert = AlertDialog(title: Text(AppLocalizations.of(context)!.enterCurrentPasswordForDelete), actions: [errorAcknowledgeButton]);
580 0 : Widget continueButton = ElevatedButton(
581 0 : child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
582 0 : onPressed: () {
583 0 : var onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
584 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteProfile(onion, ctrlrOldPass.value.text);
585 :
586 0 : Future.delayed(const Duration(milliseconds: 500), () {
587 0 : if (globalErrorHandler.deleteProfileSuccess) {
588 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteProfileSuccess + ":" + onion));
589 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
590 0 : Navigator.of(context).popUntil((route) => route.isFirst); // dismiss dialog
591 : } else {
592 0 : showDialog(
593 : context: context,
594 0 : builder: (BuildContext context) {
595 : return passwordRequiredToDeleteAlert;
596 : },
597 : );
598 : }
599 : });
600 : },
601 : );
602 :
603 : // set up the AlertDialog
604 0 : AlertDialog alert = AlertDialog(title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn), actions: [cancelButton, continueButton]);
605 :
606 : // show the dialog
607 0 : showDialog(
608 : context: context,
609 0 : builder: (BuildContext context) {
610 : return alert;
611 : },
612 : );
613 : }
614 : }
|