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