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