LCOV - code coverage report
Current view: top level - lib/views - addedithybridgroups.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 219 0.0 %
Date: 2026-08-30 22:38:45 Functions: 0 0 -

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

Generated by: LCOV version 1.14