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

Generated by: LCOV version 1.14