LCOV - code coverage report
Current view: top level - lib/views - addeditprofileview.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 340 0.0 %
Date: 2026-10-02 18:04:12 Functions: 0 0 -

          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             : }

Generated by: LCOV version 1.14