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

Generated by: LCOV version 1.14