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.copy_address, 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: Material(
253 0 : child: SwitchListTile(
254 0 : title: Text(AppLocalizations.of(context)!.profileEnabled, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor)),
255 0 : subtitle: Text(AppLocalizations.of(context)!.profileEnabledDescription),
256 0 : value: Provider.of<ProfileInfoState>(context).enabled,
257 0 : onChanged: (bool value) {
258 0 : Provider.of<ProfileInfoState>(context, listen: false).enabled = value;
259 : if (value) {
260 0 : if (Provider.of<ProfileInfoState>(context, listen: false).appearOffline == false) {
261 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(Provider.of<ProfileInfoState>(context, listen: false).onion, true, true, true);
262 : } else {
263 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(Provider.of<ProfileInfoState>(context, listen: false).onion, false, false, false);
264 : }
265 : } else {
266 0 : Provider.of<ProfileInfoState>(context, listen: false).deactivatePeerEngine(context);
267 : }
268 : },
269 0 : activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
270 0 : inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
271 0 : secondary: Icon(CwtchIcons.negative_heart_24px, color: Provider.of<Settings>(context).current().mainTextColor),
272 : ),
273 : ),
274 : ),
275 :
276 : // Auto start
277 0 : Material(
278 0 : child: SwitchListTile(
279 0 : title: Text(AppLocalizations.of(context)!.profileAutostartLabel, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor)),
280 0 : subtitle: Text(AppLocalizations.of(context)!.profileAutostartDescription),
281 0 : value: Provider.of<ProfileInfoState>(context).autostart,
282 0 : onChanged: (bool value) {
283 0 : Provider.of<ProfileInfoState>(context, listen: false).autostart = value;
284 :
285 0 : if (Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty) {
286 0 : Provider.of<FlwtchState>(
287 : context,
288 : listen: false,
289 0 : ).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.autostart", value ? "true" : "false");
290 : }
291 : },
292 0 : activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
293 0 : inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
294 0 : secondary: Icon(CwtchIcons.favorite_24px, color: Provider.of<Settings>(context).current().mainTextColor),
295 : ),
296 : ),
297 :
298 : // Appear Offline
299 0 : Visibility(
300 : // FIXME don't show the disable switch in test mode...this is a bug relating to scrolling things into view
301 0 : visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty && (EnvironmentConfig.TEST_MODE == false),
302 0 : child: Material(
303 0 : child: SwitchListTile(
304 0 : title: Text(AppLocalizations.of(context)!.profileAppearOffline, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor)),
305 0 : subtitle: Text(AppLocalizations.of(context)!.profileAppearOfflineDescription),
306 0 : value: Provider.of<ProfileInfoState>(context).appearOffline,
307 0 : onChanged: (bool value) {
308 0 : Provider.of<ProfileInfoState>(context, listen: false).appearOffline = value;
309 0 : var onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
310 0 : if (onion.isNotEmpty) {
311 0 : Provider.of<ProfileInfoState>(context, listen: false).deactivatePeerEngine(context);
312 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.appear-offline", value ? "true" : "false");
313 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ConfigureConnections(onion, !value, !value, !value);
314 : }
315 : },
316 0 : activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
317 0 : inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
318 0 : secondary: Icon(CwtchIcons.favorite_24px, color: Provider.of<Settings>(context).current().mainTextColor),
319 : ),
320 : ),
321 : ),
322 :
323 0 : Visibility(visible: Provider.of<ProfileInfoState>(context).onion.isEmpty, child: SizedBox(height: 20)),
324 0 : Visibility(
325 0 : visible: Provider.of<ProfileInfoState>(context).onion.isEmpty,
326 0 : child: Column(
327 : mainAxisAlignment: MainAxisAlignment.center,
328 0 : children: <Widget>[
329 0 : Checkbox(
330 0 : key: Key("passwordCheckBox"),
331 0 : value: usePassword,
332 0 : fillColor: MaterialStateProperty.all(theme.current().defaultButtonColor),
333 0 : activeColor: theme.current().defaultButtonActiveColor,
334 0 : onChanged: _handleSwitchPassword,
335 : ),
336 0 : Text(AppLocalizations.of(context)!.radioUsePassword, style: TextStyle(color: theme.current().mainTextColor)),
337 0 : SizedBox(height: 20),
338 0 : Padding(
339 0 : padding: EdgeInsets.symmetric(horizontal: 24),
340 0 : child: Text(
341 0 : usePassword ? AppLocalizations.of(context)!.encryptedProfileDescription : AppLocalizations.of(context)!.plainProfileDescription,
342 : textAlign: TextAlign.center,
343 : ),
344 : ),
345 : ],
346 : ),
347 : ),
348 0 : SizedBox(height: 20),
349 0 : Visibility(
350 0 : visible: usePassword,
351 0 : child: Column(
352 : mainAxisAlignment: MainAxisAlignment.start,
353 : crossAxisAlignment: CrossAxisAlignment.start,
354 0 : children: <Widget>[
355 0 : Visibility(
356 0 : visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty && Provider.of<ProfileInfoState>(context).isEncrypted,
357 0 : child: Column(
358 : mainAxisAlignment: MainAxisAlignment.start,
359 : crossAxisAlignment: CrossAxisAlignment.start,
360 0 : children: [
361 0 : CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
362 0 : SizedBox(height: 20),
363 0 : CwtchPasswordField(
364 0 : key: Key("currentPasswordFormElement"),
365 0 : controller: ctrlrOldPass,
366 0 : autoFillHints: [AutofillHints.newPassword],
367 0 : validator: (value) {
368 : // Password field can be empty when just updating the profile, not on creation
369 0 : if (Provider.of<ProfileInfoState>(context, listen: false).isEncrypted &&
370 0 : Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty &&
371 0 : value.isEmpty &&
372 0 : usePassword) {
373 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
374 : }
375 0 : if (Provider.of<ErrorHandler>(context, listen: false).deleteProfileError == true) {
376 0 : return AppLocalizations.of(context)!.enterCurrentPasswordForDelete;
377 : }
378 : return null;
379 : },
380 : ),
381 0 : SizedBox(height: 20),
382 : ],
383 : ),
384 : ),
385 0 : CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
386 0 : SizedBox(height: 20),
387 0 : CwtchPasswordField(
388 0 : key: Key("passwordFormElement"),
389 0 : controller: ctrlrPass,
390 0 : validator: (value) {
391 : // Password field can be empty when just updating the profile, not on creation
392 0 : if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
393 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
394 : }
395 0 : if (value != ctrlrPass2.value.text) {
396 0 : return AppLocalizations.of(context)!.passwordErrorMatch;
397 : }
398 : return null;
399 : },
400 : ),
401 0 : SizedBox(height: 20),
402 0 : CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
403 0 : SizedBox(height: 20),
404 0 : CwtchPasswordField(
405 0 : key: Key("confirmPasswordFormElement"),
406 0 : controller: ctrlrPass2,
407 0 : validator: (value) {
408 : // Password field can be empty when just updating the profile, not on creation
409 0 : if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
410 0 : return AppLocalizations.of(context)!.passwordErrorEmpty;
411 : }
412 0 : if (value != ctrlrPass.value.text) {
413 0 : return AppLocalizations.of(context)!.passwordErrorMatch;
414 : }
415 : return null;
416 : },
417 : ),
418 : ],
419 : ),
420 : ),
421 0 : SizedBox(height: 20),
422 0 : ElevatedButton(
423 0 : key: Key("createOrSaveProfileBtn"),
424 0 : onPressed: _createPressed,
425 0 : style: ElevatedButton.styleFrom(
426 0 : minimumSize: Size(400, 75),
427 0 : maximumSize: Size(800, 75),
428 0 : shape: RoundedRectangleBorder(
429 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
430 : ),
431 : ),
432 0 : child: Text(
433 0 : Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn,
434 : textAlign: TextAlign.center,
435 : ),
436 : ),
437 0 : SizedBox(height: 20),
438 0 : Visibility(
439 0 : visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
440 0 : child: Tooltip(
441 0 : message: AppLocalizations.of(context)!.exportProfileTooltip,
442 0 : child: OutlinedButton.icon(
443 0 : style: OutlinedButton.styleFrom(minimumSize: Size(400, 75), maximumSize: Size(800, 75)),
444 0 : onPressed: () {
445 0 : if (Platform.isAndroid) {
446 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, ctrlrOnion.value.text + ".tar.gz");
447 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + ctrlrOnion.value.text + ".tar.gz"));
448 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
449 : } else {
450 0 : showCreateFilePicker(context).then((name) {
451 : if (name != null) {
452 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, name);
453 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + name));
454 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
455 : }
456 : });
457 : }
458 : },
459 0 : icon: Icon(Icons.import_export),
460 0 : label: Text(AppLocalizations.of(context)!.exportProfile),
461 : ),
462 : ),
463 : ),
464 0 : SizedBox(height: 20),
465 0 : Visibility(
466 0 : visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
467 0 : child: Tooltip(
468 0 : message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
469 0 : child: FilledButton.icon(
470 0 : style: FilledButton.styleFrom(
471 0 : minimumSize: Size(400, 75),
472 0 : maximumSize: Size(800, 75),
473 0 : shape: RoundedRectangleBorder(
474 0 : side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
475 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
476 : ),
477 : ),
478 0 : onPressed: () {
479 0 : showAlertDialog(context);
480 : },
481 0 : icon: Icon(Icons.delete_forever),
482 0 : label: Text(AppLocalizations.of(context)!.deleteBtn),
483 : ),
484 : ),
485 : ),
486 : ],
487 : ),
488 : ),
489 : ),
490 : ),
491 : ),
492 : ),
493 : );
494 : },
495 : );
496 : },
497 : );
498 : }
499 :
500 0 : void _copyOnion() {
501 0 : Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
502 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
503 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
504 : }
505 :
506 0 : void _createPressed() async {
507 : // This will run all the validations in the form including
508 : // checking that display name is not empty, and an actual check that the passwords
509 : // match (and are provided if the user has requested an encrypted profile).
510 0 : if (_formKey.currentState!.validate()) {
511 0 : if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty) {
512 0 : if (usePassword == true) {
513 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text, Provider.of<ProfileInfoState>(context, listen: false).autostart).then((
514 : profile_id,
515 : ) {
516 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile_id, "profile.private-name", ctrlrPrivateName.value.text);
517 : });
518 : } else {
519 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, DefaultPassword, Provider.of<ProfileInfoState>(context, listen: false).autostart).then((
520 : profile_id,
521 : ) {
522 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile_id, "profile.private-name", ctrlrPrivateName.value.text);
523 : });
524 : }
525 0 : Navigator.of(context).pop();
526 : } else {
527 : // Profile Editing
528 0 : var profile = Provider.of<ProfileInfoState>(context, listen: false).onion;
529 : // update name
530 0 : Provider.of<ProfileInfoState>(context, listen: false).nickname = ctrlrNick.value.text;
531 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile, "profile.name", ctrlrNick.value.text);
532 0 : Provider.of<ProfileInfoState>(context, listen: false).setPrivateName(ctrlrPrivateName.value.text);
533 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile, "profile.private-name", ctrlrPrivateName.value.text);
534 :
535 0 : if (ctrlrPass.value.text.isEmpty) {
536 0 : Navigator.of(context).pop();
537 : } else {
538 : // At this points passwords have been validated to be the same and not empty so update password
539 : // Use default password if the profile is unencrypted
540 0 : var password = Provider.of<ProfileInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.text : DefaultPassword;
541 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ChangePassword(profile, password, ctrlrPass.text, ctrlrPass2.text);
542 :
543 0 : EnvironmentConfig.debugLog("waiting for change password response");
544 0 : Future.delayed(const Duration(milliseconds: 500), () {
545 0 : if (globalErrorHandler.changePasswordError) {
546 : // TODO: This isn't ideal, but because onChange can be fired during this future check
547 : // and because the context can change after being popped we have this kind of double assertion...
548 : // There is probably a better pattern to handle this...
549 0 : if (AppLocalizations.of(context) != null) {
550 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.passwordChangeError));
551 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
552 : return;
553 : }
554 : }
555 0 : }).whenComplete(() {
556 0 : if (globalErrorHandler.explicitChangePasswordSuccess) {
557 : // we need to set the local encrypted status to display correct password forms on this run...
558 0 : Provider.of<ProfileInfoState>(context, listen: false).isEncrypted = true;
559 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.newPassword));
560 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
561 0 : Navigator.pop(context);
562 : return; // otherwise round and round we go...
563 : }
564 : });
565 : }
566 : }
567 : }
568 : }
569 :
570 0 : showAlertDialog(BuildContext context) {
571 : // set up the buttons
572 0 : Widget cancelButton = ElevatedButton(
573 0 : child: Text(AppLocalizations.of(context)!.cancel),
574 0 : onPressed: () {
575 0 : Navigator.of(context).pop(); // dismiss dialog
576 : },
577 : );
578 : // For #939: provide an alert that a password must be provided to delete a password-protected profile.
579 0 : Widget errorAcknowledgeButton = ElevatedButton(
580 0 : child: Text(AppLocalizations.of(context)!.cancel),
581 0 : onPressed: () {
582 0 : Navigator.of(context)
583 0 : ..pop()
584 0 : ..pop(); // dismiss dialog
585 : },
586 : );
587 0 : AlertDialog passwordRequiredToDeleteAlert = AlertDialog(title: Text(AppLocalizations.of(context)!.enterCurrentPasswordForDelete), actions: [errorAcknowledgeButton]);
588 0 : Widget continueButton = ElevatedButton(
589 0 : child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
590 0 : onPressed: () {
591 0 : var onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
592 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteProfile(onion, ctrlrOldPass.value.text);
593 :
594 0 : Future.delayed(const Duration(milliseconds: 500), () {
595 0 : if (globalErrorHandler.deleteProfileSuccess) {
596 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteProfileSuccess + ":" + onion));
597 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
598 0 : Navigator.of(context).popUntil((route) => route.isFirst); // dismiss dialog
599 : } else {
600 0 : showDialog(
601 : context: context,
602 0 : builder: (BuildContext context) {
603 : return passwordRequiredToDeleteAlert;
604 : },
605 : );
606 : }
607 : });
608 : },
609 : );
610 :
611 : // set up the AlertDialog
612 0 : AlertDialog alert = AlertDialog(title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn), actions: [cancelButton, continueButton]);
613 :
614 : // show the dialog
615 0 : showDialog(
616 : context: context,
617 0 : builder: (BuildContext context) {
618 : return alert;
619 : },
620 : );
621 : }
622 : }
|