]>
Commit | Line | Data |
---|---|---|
0e468546 | 1 | import 'dart:io'; |
a9d1ee22 | 2 | import 'dart:async'; |
0e468546 TM |
3 | |
4 | import 'package:flutter/material.dart'; | |
5 | import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart' | |
6 | as proxclient; | |
7 | import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'; | |
8 | import 'package:proxmox_login_manager/proxmox_general_settings_form.dart'; | |
9 | import 'package:proxmox_login_manager/proxmox_general_settings_model.dart'; | |
10 | import 'package:proxmox_login_manager/proxmox_login_model.dart'; | |
11 | import 'package:proxmox_login_manager/proxmox_tfa_form.dart'; | |
1dfe0c76 | 12 | import 'package:proxmox_login_manager/extension.dart'; |
0e468546 TM |
13 | |
14 | class ProxmoxProgressModel { | |
a9d1ee22 TL |
15 | bool inProgress = false; |
16 | String message = 'Loading...'; | |
0e468546 TM |
17 | ProxmoxProgressModel({ |
18 | this.inProgress = false, | |
19 | this.message = 'Loading...', | |
20 | }); | |
21 | } | |
22 | ||
23 | class ProxmoxLoginForm extends StatefulWidget { | |
24 | final TextEditingController originController; | |
25 | final FormFieldValidator<String> originValidator; | |
26 | final TextEditingController usernameController; | |
27 | final TextEditingController passwordController; | |
a9d1ee22 TL |
28 | final List<PveAccessDomainModel?>? accessDomains; |
29 | final PveAccessDomainModel? selectedDomain; | |
30 | final ValueChanged<PveAccessDomainModel?> onDomainChanged; | |
31 | final Function? onPasswordSubmitted; | |
32 | final Function? onOriginSubmitted; | |
0e468546 TM |
33 | |
34 | const ProxmoxLoginForm({ | |
a9d1ee22 TL |
35 | Key? key, |
36 | /*required*/ required this.originController, | |
37 | /*required*/ /*required*/ required this.usernameController, | |
38 | /*required*/ required this.passwordController, | |
39 | /*required*/ required this.accessDomains, | |
40 | /*required*/ required this.originValidator, | |
0e468546 | 41 | this.selectedDomain, |
a9d1ee22 | 42 | /*required*/ required this.onDomainChanged, |
c2ebed05 TM |
43 | this.onPasswordSubmitted, |
44 | this.onOriginSubmitted, | |
0e468546 TM |
45 | }) : super(key: key); |
46 | ||
47 | @override | |
48 | _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState(); | |
49 | } | |
50 | ||
51 | class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { | |
52 | bool _obscure = true; | |
a9d1ee22 | 53 | FocusNode? passwordFocusNode; |
0e468546 | 54 | |
0e468546 TM |
55 | @override |
56 | Widget build(BuildContext context) { | |
57 | if (widget.accessDomains == null) { | |
58 | return TextFormField( | |
59 | decoration: InputDecoration( | |
60 | icon: Icon(Icons.vpn_lock), | |
61 | labelText: 'Origin', | |
62 | hintText: 'e.g. 192.168.1.2', | |
63 | helperText: 'Protocol (https) and default port (8006) implied'), | |
64 | controller: widget.originController, | |
65 | validator: widget.originValidator, | |
a9d1ee22 | 66 | onFieldSubmitted: (value) => widget.onOriginSubmitted!(), |
0e468546 TM |
67 | ); |
68 | } | |
69 | ||
eeba8b50 TM |
70 | return AutofillGroup( |
71 | child: Column( | |
72 | mainAxisAlignment: MainAxisAlignment.center, | |
73 | children: [ | |
74 | TextFormField( | |
75 | decoration: InputDecoration( | |
76 | icon: Icon(Icons.vpn_lock), | |
77 | labelText: 'Origin', | |
78 | ), | |
79 | controller: widget.originController, | |
80 | enabled: false, | |
0e468546 | 81 | ), |
eeba8b50 TM |
82 | TextFormField( |
83 | decoration: InputDecoration( | |
84 | icon: Icon(Icons.person), | |
85 | labelText: 'Username', | |
0e468546 | 86 | ), |
eeba8b50 TM |
87 | controller: widget.usernameController, |
88 | validator: (value) { | |
a9d1ee22 | 89 | if (value!.isEmpty) { |
eeba8b50 TM |
90 | return 'Please enter username'; |
91 | } | |
92 | return null; | |
93 | }, | |
94 | autofillHints: [AutofillHints.username], | |
95 | ), | |
96 | DropdownButtonFormField( | |
97 | decoration: InputDecoration(icon: Icon(Icons.domain)), | |
a9d1ee22 | 98 | items: widget.accessDomains! |
eeba8b50 TM |
99 | .map((e) => DropdownMenuItem( |
100 | child: ListTile( | |
a9d1ee22 | 101 | title: Text(e!.realm!), |
eeba8b50 TM |
102 | subtitle: Text(e.comment ?? ''), |
103 | ), | |
104 | value: e, | |
105 | )) | |
106 | .toList(), | |
107 | onChanged: widget.onDomainChanged, | |
108 | selectedItemBuilder: (context) => | |
a9d1ee22 | 109 | widget.accessDomains!.map((e) => Text(e!.realm!)).toList(), |
eeba8b50 TM |
110 | value: widget.selectedDomain, |
111 | ), | |
112 | Stack( | |
113 | children: [ | |
114 | TextFormField( | |
115 | decoration: InputDecoration( | |
116 | icon: Icon(Icons.lock), | |
117 | labelText: 'Password', | |
118 | ), | |
119 | controller: widget.passwordController, | |
120 | obscureText: _obscure, | |
121 | autocorrect: false, | |
122 | focusNode: passwordFocusNode, | |
123 | validator: (value) { | |
a9d1ee22 | 124 | if (value!.isEmpty) { |
eeba8b50 TM |
125 | return 'Please enter password'; |
126 | } | |
127 | return null; | |
128 | }, | |
a9d1ee22 | 129 | onFieldSubmitted: (value) => widget.onPasswordSubmitted!(), |
eeba8b50 | 130 | autofillHints: [AutofillHints.password], |
0e468546 | 131 | ), |
eeba8b50 TM |
132 | Align( |
133 | alignment: Alignment.bottomRight, | |
134 | child: IconButton( | |
135 | constraints: BoxConstraints.tight(Size(58, 58)), | |
136 | iconSize: 24, | |
137 | icon: | |
138 | Icon(_obscure ? Icons.visibility : Icons.visibility_off), | |
139 | onPressed: () => setState(() { | |
140 | _obscure = !_obscure; | |
141 | }), | |
142 | ), | |
143 | ) | |
144 | ], | |
145 | ), | |
146 | ], | |
147 | ), | |
0e468546 TM |
148 | ); |
149 | } | |
150 | ||
151 | @override | |
152 | void dispose() { | |
153 | passwordFocusNode?.dispose(); | |
154 | super.dispose(); | |
155 | } | |
156 | } | |
157 | ||
158 | class ProxmoxLoginPage extends StatefulWidget { | |
a9d1ee22 TL |
159 | final ProxmoxLoginModel? userModel; |
160 | final bool? isCreate; | |
161 | final String? ticket; | |
0e468546 TM |
162 | |
163 | const ProxmoxLoginPage({ | |
a9d1ee22 | 164 | Key? key, |
0e468546 TM |
165 | this.userModel, |
166 | this.isCreate, | |
1dfe0c76 | 167 | this.ticket = '', |
0e468546 TM |
168 | }) : super(key: key); |
169 | @override | |
170 | _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState(); | |
171 | } | |
172 | ||
173 | class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { | |
174 | final _originController = TextEditingController(); | |
175 | final _usernameController = TextEditingController(); | |
176 | final _passwordController = TextEditingController(); | |
a9d1ee22 TL |
177 | Future<List<PveAccessDomainModel?>?>? _accessDomains; |
178 | PveAccessDomainModel? _selectedDomain; | |
0e468546 | 179 | final _formKey = GlobalKey<FormState>(); |
a9d1ee22 | 180 | ProxmoxProgressModel _progressModel = ProxmoxProgressModel(); |
0e468546 | 181 | bool _submittButtonEnabled = true; |
1dfe0c76 | 182 | |
0e468546 TM |
183 | @override |
184 | void initState() { | |
185 | super.initState(); | |
186 | final userModel = widget.userModel; | |
187 | _progressModel = ProxmoxProgressModel(); | |
a9d1ee22 | 188 | if (!widget.isCreate! && userModel != null) { |
0e468546 TM |
189 | _progressModel |
190 | ..inProgress = true | |
191 | ..message = 'Connection test...'; | |
192 | _originController.text = | |
193 | '${userModel.origin?.host}:${userModel.origin?.port}'; | |
194 | _accessDomains = _getAccessDomains(); | |
a9d1ee22 TL |
195 | _usernameController.text = userModel.username!; |
196 | if (widget.ticket!.isNotEmpty && userModel.activeSession) { | |
197 | _onLoginButtonPressed(ticket: widget.ticket!, mRealm: userModel.realm); | |
1dfe0c76 | 198 | } |
0e468546 TM |
199 | } |
200 | } | |
201 | ||
202 | @override | |
203 | Widget build(BuildContext context) { | |
204 | return Theme( | |
205 | data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)), | |
5589fc9c TM |
206 | child: Scaffold( |
207 | backgroundColor: Theme.of(context).primaryColor, | |
ab9e420b TM |
208 | extendBodyBehindAppBar: true, |
209 | appBar: AppBar( | |
210 | elevation: 0.0, | |
211 | backgroundColor: Colors.transparent, | |
212 | leading: IconButton( | |
213 | icon: Icon(Icons.close), | |
214 | onPressed: () => Navigator.of(context).pop(), | |
215 | ), | |
216 | ), | |
5589fc9c | 217 | body: Stack( |
0e468546 TM |
218 | children: [ |
219 | SingleChildScrollView( | |
220 | child: ConstrainedBox( | |
221 | constraints: BoxConstraints.tightFor( | |
222 | height: MediaQuery.of(context).size.height), | |
223 | child: Padding( | |
224 | padding: const EdgeInsets.all(8.0), | |
a9d1ee22 | 225 | child: FutureBuilder<List<PveAccessDomainModel?>?>( |
0e468546 TM |
226 | future: _accessDomains, |
227 | builder: (context, snapshot) { | |
228 | return Form( | |
229 | key: _formKey, | |
230 | onChanged: () { | |
231 | setState(() { | |
232 | _submittButtonEnabled = | |
a9d1ee22 | 233 | _formKey.currentState!.validate(); |
0e468546 TM |
234 | }); |
235 | }, | |
236 | child: Column( | |
237 | mainAxisAlignment: MainAxisAlignment.center, | |
238 | children: [ | |
239 | Expanded( | |
240 | child: Container( | |
241 | child: Column( | |
242 | mainAxisAlignment: MainAxisAlignment.center, | |
243 | children: [ | |
dadc4428 TM |
244 | Image.asset( |
245 | 'assets/images/proxmox_logo_symbol_wordmark.png', | |
246 | package: 'proxmox_login_manager', | |
0e468546 TM |
247 | ), |
248 | ], | |
249 | ), | |
250 | ), | |
251 | ), | |
252 | ProxmoxLoginForm( | |
253 | originController: _originController, | |
254 | originValidator: (value) { | |
a9d1ee22 | 255 | if (value == null || value.isEmpty) { |
0e468546 TM |
256 | return 'Please enter origin'; |
257 | } | |
258 | if (value.startsWith('https://') || | |
259 | value.startsWith('http://')) { | |
260 | return 'Do not prefix with scheme'; | |
261 | } | |
262 | try { | |
263 | Uri.https(value, ''); | |
264 | return null; | |
265 | } on FormatException catch (_) { | |
266 | return 'Invalid URI'; | |
267 | } | |
268 | }, | |
269 | usernameController: _usernameController, | |
270 | passwordController: _passwordController, | |
271 | accessDomains: snapshot.data, | |
272 | selectedDomain: _selectedDomain, | |
273 | onDomainChanged: (value) { | |
274 | setState(() { | |
275 | _selectedDomain = value; | |
276 | }); | |
277 | }, | |
c2ebed05 TM |
278 | onOriginSubmitted: _submittButtonEnabled |
279 | ? () { | |
280 | final isValid = | |
a9d1ee22 | 281 | _formKey.currentState!.validate(); |
c2ebed05 TM |
282 | setState(() { |
283 | _submittButtonEnabled = isValid; | |
284 | }); | |
285 | if (isValid) { | |
286 | setState(() { | |
287 | _accessDomains = | |
288 | _getAccessDomains(); | |
289 | }); | |
290 | } | |
291 | } | |
292 | : null, | |
293 | onPasswordSubmitted: _submittButtonEnabled | |
294 | ? () { | |
295 | final isValid = | |
a9d1ee22 | 296 | _formKey.currentState!.validate(); |
c2ebed05 TM |
297 | setState(() { |
298 | _submittButtonEnabled = isValid; | |
299 | }); | |
300 | if (isValid) { | |
301 | _onLoginButtonPressed(); | |
302 | } | |
303 | } | |
304 | : null, | |
0e468546 TM |
305 | ), |
306 | if (snapshot.hasData) | |
307 | Expanded( | |
308 | child: Align( | |
309 | alignment: Alignment.bottomCenter, | |
310 | child: Container( | |
311 | width: MediaQuery.of(context).size.width, | |
312 | child: FlatButton( | |
313 | onPressed: _submittButtonEnabled | |
314 | ? () { | |
315 | final isValid = _formKey | |
a9d1ee22 | 316 | .currentState! |
0e468546 TM |
317 | .validate(); |
318 | setState(() { | |
319 | _submittButtonEnabled = | |
320 | isValid; | |
321 | }); | |
322 | if (isValid) { | |
323 | _onLoginButtonPressed(); | |
324 | } | |
325 | } | |
326 | : null, | |
327 | color: Color(0xFFE47225), | |
328 | disabledColor: Colors.grey, | |
329 | child: Text('Continue'), | |
330 | ), | |
331 | ), | |
332 | ), | |
333 | ), | |
334 | if (!snapshot.hasData) | |
335 | Expanded( | |
336 | child: Align( | |
337 | alignment: Alignment.bottomCenter, | |
338 | child: Container( | |
339 | width: MediaQuery.of(context).size.width, | |
340 | child: FlatButton( | |
341 | onPressed: _submittButtonEnabled | |
342 | ? () { | |
343 | final isValid = _formKey | |
a9d1ee22 | 344 | .currentState! |
0e468546 TM |
345 | .validate(); |
346 | setState(() { | |
347 | _submittButtonEnabled = | |
348 | isValid; | |
349 | }); | |
350 | if (isValid) { | |
351 | setState(() { | |
352 | _accessDomains = | |
353 | _getAccessDomains(); | |
354 | }); | |
355 | } | |
356 | } | |
357 | : null, | |
358 | color: Color(0xFFE47225), | |
359 | child: Text('Continue'), | |
360 | disabledColor: Colors.grey, | |
361 | ), | |
362 | ), | |
363 | ), | |
364 | ), | |
365 | ], | |
366 | ), | |
367 | ); | |
368 | }), | |
369 | ), | |
370 | ), | |
371 | ), | |
372 | if (_progressModel.inProgress) | |
373 | ProxmoxProgressOverlay(message: _progressModel.message), | |
374 | ], | |
375 | ), | |
376 | ), | |
377 | ); | |
378 | } | |
379 | ||
1dfe0c76 | 380 | Future<void> _onLoginButtonPressed( |
a9d1ee22 | 381 | {String ticket = '', String? mRealm}) async { |
0e468546 TM |
382 | setState(() { |
383 | _progressModel | |
384 | ..inProgress = true | |
385 | ..message = 'Authenticating...'; | |
386 | }); | |
387 | ||
388 | try { | |
389 | final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); | |
3752cb66 TM |
390 | |
391 | //cleaned form fields | |
392 | final origin = Uri.https(_originController.text.trim(), ''); | |
393 | final username = _usernameController.text.trim(); | |
1dfe0c76 TM |
394 | final password = |
395 | ticket.isNotEmpty ? ticket : _passwordController.text.trim(); | |
396 | final realm = _selectedDomain?.realm ?? mRealm; | |
3752cb66 | 397 | |
0e468546 | 398 | var client = await proxclient.authenticate( |
a9d1ee22 | 399 | '$username@$realm', password, origin, settings.sslValidation!); |
0e468546 TM |
400 | |
401 | if (client.credentials.tfa) { | |
a9d1ee22 | 402 | client = await (Navigator.of(context).push(MaterialPageRoute( |
0e468546 TM |
403 | builder: (context) => ProxmoxTfaForm( |
404 | apiClient: client, | |
405 | ), | |
a9d1ee22 | 406 | )) as FutureOr<ProxmoxApiClient>); |
0e468546 TM |
407 | } |
408 | ||
1dfe0c76 TM |
409 | final status = await client.getClusterStatus(); |
410 | final hostname = | |
a9d1ee22 | 411 | status.singleWhere((element) => element!.local ?? false)!.name; |
0e468546 TM |
412 | var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); |
413 | ||
a9d1ee22 | 414 | if (widget.isCreate!) { |
0e468546 TM |
415 | final newLogin = ProxmoxLoginModel((b) => b |
416 | ..origin = origin | |
3752cb66 TM |
417 | ..username = username |
418 | ..realm = realm | |
0e468546 | 419 | ..productType = ProxmoxProductType.pve |
1dfe0c76 TM |
420 | ..ticket = client.credentials.ticket |
421 | ..hostname = hostname); | |
3752cb66 | 422 | |
a9d1ee22 | 423 | loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); |
0e468546 | 424 | } else { |
a9d1ee22 | 425 | loginStorage = loginStorage!.rebuild((b) => b |
1dfe0c76 TM |
426 | ..logins.rebuildWhere( |
427 | (m) => m == widget.userModel, | |
428 | (b) => b | |
429 | ..ticket = client.credentials.ticket | |
430 | ..hostname = hostname)); | |
0e468546 TM |
431 | } |
432 | await loginStorage.saveToDisk(); | |
433 | ||
434 | Navigator.of(context).pop(client); | |
435 | } on proxclient.ProxmoxApiException catch (e) { | |
436 | print(e); | |
f6cf3349 TM |
437 | if (e.message.contains('No ticket')) { |
438 | showDialog( | |
439 | context: context, | |
440 | builder: (context) => AlertDialog( | |
441 | title: Text('Version Error'), | |
442 | content: Text( | |
443 | 'Proxmox VE version not supported, please update your instance to use this app.'), | |
444 | actions: [ | |
445 | FlatButton( | |
446 | onPressed: () => Navigator.of(context).pop(), | |
447 | child: Text('Close'), | |
448 | ), | |
449 | ], | |
450 | ), | |
451 | ); | |
452 | } else { | |
453 | showDialog( | |
454 | context: context, | |
455 | builder: (context) => ProxmoxApiErrorDialog( | |
456 | exception: e, | |
457 | ), | |
458 | ); | |
459 | } | |
0e468546 TM |
460 | } catch (e, trace) { |
461 | print(e); | |
462 | print(trace); | |
463 | if (e.runtimeType == HandshakeException) { | |
464 | showDialog( | |
465 | context: context, | |
466 | builder: (context) => ProxmoxCertificateErrorDialog(), | |
467 | ); | |
468 | } | |
469 | } | |
470 | setState(() { | |
471 | _progressModel.inProgress = false; | |
472 | }); | |
473 | } | |
474 | ||
a9d1ee22 | 475 | Future<List<PveAccessDomainModel?>?> _getAccessDomains() async { |
0e468546 TM |
476 | setState(() { |
477 | _progressModel | |
478 | ..inProgress = true | |
479 | ..message = 'Connection test...'; | |
480 | }); | |
59ab8956 TL |
481 | var host = _originController.text.trim(); |
482 | var apiBaseUrl = Uri.https(host, ''); | |
0e468546 | 483 | |
59ab8956 TL |
484 | RegExp portRE = new RegExp(r":\d{1,5}$"); |
485 | ||
486 | if (!portRE.hasMatch(host)) { | |
0e468546 TM |
487 | _originController.text += ':8006'; |
488 | apiBaseUrl = apiBaseUrl.replace(port: 8006); | |
489 | } | |
490 | ||
491 | final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); | |
a9d1ee22 | 492 | List<PveAccessDomainModel?>? response; |
0e468546 TM |
493 | try { |
494 | response = | |
a9d1ee22 | 495 | await proxclient.accessDomains(apiBaseUrl, settings.sslValidation!); |
0e468546 | 496 | } on proxclient.ProxmoxApiException catch (e) { |
0e468546 TM |
497 | showDialog( |
498 | context: context, | |
499 | builder: (context) => ProxmoxApiErrorDialog( | |
500 | exception: e, | |
501 | ), | |
502 | ); | |
503 | } catch (e, trace) { | |
504 | print(e); | |
505 | print(trace); | |
506 | if (e.runtimeType == HandshakeException) { | |
507 | showDialog( | |
508 | context: context, | |
509 | builder: (context) => ProxmoxCertificateErrorDialog(), | |
510 | ); | |
511 | } else { | |
512 | showDialog( | |
513 | context: context, | |
514 | builder: (context) => AlertDialog( | |
515 | title: Text('Connection error'), | |
516 | content: Text('Could not establish connection.'), | |
517 | actions: [ | |
518 | FlatButton( | |
519 | onPressed: () => Navigator.of(context).pop(), | |
520 | child: Text('Close'), | |
521 | ), | |
522 | ], | |
523 | ), | |
524 | ); | |
525 | } | |
526 | } | |
3752cb66 | 527 | |
a9d1ee22 | 528 | response?.sort((a, b) => a!.realm!.compareTo(b!.realm!)); |
3752cb66 | 529 | |
0e468546 | 530 | final selection = response?.singleWhere( |
a9d1ee22 | 531 | (e) => e!.realm == widget.userModel?.realm, |
0e468546 TM |
532 | orElse: () => response?.first, |
533 | ); | |
3752cb66 | 534 | |
0e468546 TM |
535 | setState(() { |
536 | _progressModel.inProgress = false; | |
537 | _selectedDomain = selection; | |
538 | }); | |
3752cb66 | 539 | |
0e468546 TM |
540 | return response; |
541 | } | |
542 | ||
543 | @override | |
544 | void dispose() { | |
545 | _originController.dispose(); | |
546 | _usernameController.dispose(); | |
547 | _passwordController.dispose(); | |
548 | super.dispose(); | |
549 | } | |
550 | } | |
551 | ||
552 | class ProxmoxProgressOverlay extends StatelessWidget { | |
553 | const ProxmoxProgressOverlay({ | |
a9d1ee22 TL |
554 | Key? key, |
555 | required this.message, | |
0e468546 TM |
556 | }) : super(key: key); |
557 | ||
558 | final String message; | |
559 | ||
560 | @override | |
561 | Widget build(BuildContext context) { | |
562 | return Container( | |
563 | decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)), | |
564 | child: Center( | |
565 | child: Column( | |
566 | mainAxisAlignment: MainAxisAlignment.center, | |
567 | children: [ | |
568 | Text( | |
569 | message, | |
570 | style: TextStyle( | |
571 | color: Theme.of(context).accentColor, | |
572 | fontSize: 20, | |
573 | ), | |
574 | ), | |
575 | Padding( | |
576 | padding: const EdgeInsets.only(top: 20.0), | |
577 | child: CircularProgressIndicator(), | |
578 | ) | |
579 | ], | |
580 | ), | |
581 | ), | |
582 | ); | |
583 | } | |
584 | } | |
585 | ||
586 | class ProxmoxApiErrorDialog extends StatelessWidget { | |
587 | final proxclient.ProxmoxApiException exception; | |
588 | ||
589 | const ProxmoxApiErrorDialog({ | |
a9d1ee22 TL |
590 | Key? key, |
591 | required this.exception, | |
0e468546 TM |
592 | }) : super(key: key); |
593 | ||
594 | @override | |
595 | Widget build(BuildContext context) { | |
596 | return AlertDialog( | |
597 | title: Text('API Error'), | |
598 | content: SingleChildScrollView( | |
599 | child: Text(exception.message), | |
600 | ), | |
601 | actions: [ | |
602 | FlatButton( | |
603 | onPressed: () => Navigator.of(context).pop(), | |
604 | child: Text('Close'), | |
605 | ), | |
606 | ], | |
607 | ); | |
608 | } | |
609 | } | |
610 | ||
611 | class ProxmoxCertificateErrorDialog extends StatelessWidget { | |
612 | const ProxmoxCertificateErrorDialog({ | |
a9d1ee22 | 613 | Key? key, |
0e468546 TM |
614 | }) : super(key: key); |
615 | ||
616 | @override | |
617 | Widget build(BuildContext context) { | |
618 | return AlertDialog( | |
619 | title: Text('Certificate error'), | |
620 | content: SingleChildScrollView( | |
621 | child: Column( | |
622 | crossAxisAlignment: CrossAxisAlignment.start, | |
623 | children: [ | |
624 | Text('Your connection is not private.'), | |
625 | Text( | |
626 | 'Note: Consider to disable SSL validation,' | |
627 | ' if you use a self signed, not commonly trusted, certificate.', | |
628 | style: Theme.of(context).textTheme.caption, | |
629 | ), | |
630 | ], | |
631 | ), | |
632 | ), | |
633 | actions: [ | |
634 | FlatButton( | |
635 | onPressed: () => Navigator.of(context).pop(), | |
636 | child: Text('Close'), | |
637 | ), | |
638 | FlatButton( | |
639 | onPressed: () => Navigator.of(context).pushReplacement( | |
640 | MaterialPageRoute( | |
641 | builder: (context) => ProxmoxGeneralSettingsForm())), | |
642 | child: Text('Settings'), | |
643 | ) | |
644 | ], | |
645 | ); | |
646 | } | |
647 | } |