]> git.proxmox.com Git - flutter/proxmox_login_manager.git/blame - lib/proxmox_login_form.dart
tree wide: avoid unnecessary this
[flutter/proxmox_login_manager.git] / lib / proxmox_login_form.dart
CommitLineData
0e468546 1import 'dart:io';
a9d1ee22 2import 'dart:async';
0e468546
TM
3
4import 'package:flutter/material.dart';
f0291f5a 5import 'package:collection/src/iterable_extensions.dart';
0e468546
TM
6import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
7 as proxclient;
8import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
9import 'package:proxmox_login_manager/proxmox_general_settings_form.dart';
10import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
11import 'package:proxmox_login_manager/proxmox_login_model.dart';
12import 'package:proxmox_login_manager/proxmox_tfa_form.dart';
1dfe0c76 13import 'package:proxmox_login_manager/extension.dart';
62145778 14import 'package:proxmox_login_manager/proxmox_password_store.dart';
0e468546
TM
15
16class ProxmoxProgressModel {
cff301db 17 int inProgress = 0;
a9d1ee22 18 String message = 'Loading...';
0e468546 19 ProxmoxProgressModel({
cff301db 20 this.inProgress = 0,
0e468546
TM
21 this.message = 'Loading...',
22 });
23}
24
f0291f5a
TL
25// FIXME: copied from pve_flutter_frontend, re-use common set
26class ProxmoxColors {
7c043135
TL
27 static const Color orange = Color(0xFFE57000);
28 static const Color supportGrey = Color(0xFFABBABA);
29 static const Color supportBlue = Color(0xFF00617F);
f0291f5a
TL
30}
31
0e468546
TM
32class ProxmoxLoginForm extends StatefulWidget {
33 final TextEditingController originController;
34 final FormFieldValidator<String> originValidator;
35 final TextEditingController usernameController;
36 final TextEditingController passwordController;
a9d1ee22
TL
37 final List<PveAccessDomainModel?>? accessDomains;
38 final PveAccessDomainModel? selectedDomain;
39 final ValueChanged<PveAccessDomainModel?> onDomainChanged;
40 final Function? onPasswordSubmitted;
06ebbd20 41 final Function onOriginSubmitted;
62145778
DC
42 final Function? onSavePasswordChanged;
43 final bool? canSavePassword;
44 final bool? passwordSaved;
0e468546
TM
45
46 const ProxmoxLoginForm({
a9d1ee22 47 Key? key,
f0291f5a
TL
48 required this.originController,
49 required this.usernameController,
50 required this.passwordController,
51 required this.accessDomains,
52 required this.originValidator,
0e468546 53 this.selectedDomain,
f0291f5a 54 required this.onDomainChanged,
c2ebed05 55 this.onPasswordSubmitted,
06ebbd20 56 required this.onOriginSubmitted,
62145778
DC
57 this.onSavePasswordChanged,
58 this.canSavePassword,
59 this.passwordSaved,
0e468546
TM
60 }) : super(key: key);
61
62 @override
63 _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState();
64}
65
66class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
67 bool _obscure = true;
62145778 68 bool? _savePwCheckbox;
a9d1ee22 69 FocusNode? passwordFocusNode;
0e468546 70
0e468546
TM
71 @override
72 Widget build(BuildContext context) {
73 if (widget.accessDomains == null) {
74 return TextFormField(
7c043135 75 decoration: const InputDecoration(
0e468546
TM
76 icon: Icon(Icons.vpn_lock),
77 labelText: 'Origin',
78 hintText: 'e.g. 192.168.1.2',
f591bbbe
DC
79 helperText:
80 'Protocol (https) and default port (8006 or 443) implied'),
3e95b235 81 textInputAction: TextInputAction.next,
0e468546
TM
82 controller: widget.originController,
83 validator: widget.originValidator,
06ebbd20 84 onFieldSubmitted: (value) => widget.onOriginSubmitted(),
0e468546
TM
85 );
86 }
87
eeba8b50
TM
88 return AutofillGroup(
89 child: Column(
90 mainAxisAlignment: MainAxisAlignment.center,
91 children: [
92 TextFormField(
7c043135 93 decoration: const InputDecoration(
eeba8b50
TM
94 icon: Icon(Icons.vpn_lock),
95 labelText: 'Origin',
96 ),
97 controller: widget.originController,
98 enabled: false,
0e468546 99 ),
eeba8b50 100 TextFormField(
7c043135 101 decoration: const InputDecoration(
eeba8b50
TM
102 icon: Icon(Icons.person),
103 labelText: 'Username',
0e468546 104 ),
eeba8b50
TM
105 controller: widget.usernameController,
106 validator: (value) {
a9d1ee22 107 if (value!.isEmpty) {
eeba8b50
TM
108 return 'Please enter username';
109 }
110 return null;
111 },
7824e4fc 112 autofillHints: const [AutofillHints.username],
eeba8b50
TM
113 ),
114 DropdownButtonFormField(
7c043135 115 decoration: const InputDecoration(icon: Icon(Icons.domain)),
a9d1ee22 116 items: widget.accessDomains!
eeba8b50 117 .map((e) => DropdownMenuItem(
7824e4fc 118 value: e,
eeba8b50 119 child: ListTile(
f0291f5a 120 title: Text(e!.realm),
eeba8b50
TM
121 subtitle: Text(e.comment ?? ''),
122 ),
eeba8b50
TM
123 ))
124 .toList(),
125 onChanged: widget.onDomainChanged,
126 selectedItemBuilder: (context) =>
f0291f5a 127 widget.accessDomains!.map((e) => Text(e!.realm)).toList(),
eeba8b50
TM
128 value: widget.selectedDomain,
129 ),
130 Stack(
131 children: [
132 TextFormField(
7c043135 133 decoration: const InputDecoration(
eeba8b50
TM
134 icon: Icon(Icons.lock),
135 labelText: 'Password',
136 ),
137 controller: widget.passwordController,
138 obscureText: _obscure,
139 autocorrect: false,
140 focusNode: passwordFocusNode,
141 validator: (value) {
a9d1ee22 142 if (value!.isEmpty) {
eeba8b50
TM
143 return 'Please enter password';
144 }
145 return null;
146 },
a9d1ee22 147 onFieldSubmitted: (value) => widget.onPasswordSubmitted!(),
7824e4fc 148 autofillHints: const [AutofillHints.password],
0e468546 149 ),
eeba8b50
TM
150 Align(
151 alignment: Alignment.bottomRight,
152 child: IconButton(
7c043135 153 constraints: BoxConstraints.tight(const Size(58, 58)),
eeba8b50
TM
154 iconSize: 24,
155 icon:
156 Icon(_obscure ? Icons.visibility : Icons.visibility_off),
157 onPressed: () => setState(() {
158 _obscure = !_obscure;
159 }),
160 ),
161 )
162 ],
163 ),
62145778
DC
164 if (widget.canSavePassword ?? false)
165 CheckboxListTile(
166 title: const Text('Save password'),
167 value: _savePwCheckbox ?? widget.passwordSaved ?? false,
168 onChanged: (value) {
169 if (widget.onSavePasswordChanged != null) {
170 widget.onSavePasswordChanged!(value!);
171 }
172 setState(() {
173 _savePwCheckbox = value!;
174 });
175 },
176 )
eeba8b50
TM
177 ],
178 ),
0e468546
TM
179 );
180 }
181
182 @override
183 void dispose() {
184 passwordFocusNode?.dispose();
185 super.dispose();
186 }
187}
188
189class ProxmoxLoginPage extends StatefulWidget {
a9d1ee22
TL
190 final ProxmoxLoginModel? userModel;
191 final bool? isCreate;
192 final String? ticket;
62145778 193 final String? password;
0e468546
TM
194
195 const ProxmoxLoginPage({
a9d1ee22 196 Key? key,
0e468546
TM
197 this.userModel,
198 this.isCreate,
1dfe0c76 199 this.ticket = '',
62145778 200 this.password,
0e468546
TM
201 }) : super(key: key);
202 @override
203 _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
204}
205
206class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
207 final _originController = TextEditingController();
208 final _usernameController = TextEditingController();
209 final _passwordController = TextEditingController();
a9d1ee22
TL
210 Future<List<PveAccessDomainModel?>?>? _accessDomains;
211 PveAccessDomainModel? _selectedDomain;
0e468546 212 final _formKey = GlobalKey<FormState>();
a9d1ee22 213 ProxmoxProgressModel _progressModel = ProxmoxProgressModel();
0e468546 214 bool _submittButtonEnabled = true;
62145778
DC
215 bool _canSavePassword = false;
216 bool _savePasswordCB = false;
1dfe0c76 217
0e468546
TM
218 @override
219 void initState() {
220 super.initState();
221 final userModel = widget.userModel;
222 _progressModel = ProxmoxProgressModel();
a9d1ee22 223 if (!widget.isCreate! && userModel != null) {
e2515b17 224 _originController.text = userModel.origin?.toString() ?? '';
f591bbbe
DC
225 // Uri does not append 443 for https, so we do it manually
226 if (userModel.origin != null &&
227 userModel.origin!.scheme == "https" &&
228 userModel.origin!.port == 443) {
229 _originController.text += ":443";
230 }
62145778 231 _passwordController.text = widget.password ?? '';
0e468546 232 _accessDomains = _getAccessDomains();
a9d1ee22 233 _usernameController.text = userModel.username!;
62145778
DC
234 _savePasswordCB = widget.password != null;
235 if ((widget.ticket!.isNotEmpty && userModel.activeSession) ||
236 widget.password != null) {
a9d1ee22 237 _onLoginButtonPressed(ticket: widget.ticket!, mRealm: userModel.realm);
1dfe0c76 238 }
0e468546
TM
239 }
240 }
241
242 @override
243 Widget build(BuildContext context) {
244 return Theme(
f0291f5a
TL
245 //data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
246 data: ThemeData.dark().copyWith(
fcdfb148
DC
247 textButtonTheme: TextButtonThemeData(
248 style: TextButton.styleFrom(
249 foregroundColor: Colors.white,
250 backgroundColor: ProxmoxColors.orange,
251 disabledBackgroundColor: Colors.grey,
252 ),
253 ),
7c043135 254 colorScheme: const ColorScheme.dark().copyWith(
2f3654f9 255 primary: ProxmoxColors.orange,
fcdfb148 256 secondary: ProxmoxColors.orange,
2f3654f9 257 onSecondary: ProxmoxColors.supportGrey),
f37413f7
TL
258 checkboxTheme: CheckboxThemeData(
259 fillColor: MaterialStateProperty.resolveWith<Color?>(
260 (Set<MaterialState> states) {
261 if (states.contains(MaterialState.disabled)) {
262 return null;
263 }
264 if (states.contains(MaterialState.selected)) {
265 return ProxmoxColors.orange;
266 }
267 return null;
268 }),
269 ),
270 radioTheme: RadioThemeData(
271 fillColor: MaterialStateProperty.resolveWith<Color?>(
272 (Set<MaterialState> states) {
273 if (states.contains(MaterialState.disabled)) {
274 return null;
275 }
276 if (states.contains(MaterialState.selected)) {
277 return ProxmoxColors.orange;
278 }
279 return null;
280 }),
281 ),
282 switchTheme: SwitchThemeData(
283 thumbColor: MaterialStateProperty.resolveWith<Color?>(
284 (Set<MaterialState> states) {
285 if (states.contains(MaterialState.disabled)) {
286 return null;
287 }
288 if (states.contains(MaterialState.selected)) {
289 return ProxmoxColors.orange;
290 }
291 return null;
292 }),
293 trackColor: MaterialStateProperty.resolveWith<Color?>(
294 (Set<MaterialState> states) {
295 if (states.contains(MaterialState.disabled)) {
296 return null;
297 }
298 if (states.contains(MaterialState.selected)) {
299 return ProxmoxColors.orange;
300 }
301 return null;
302 }),
303 ),
fcdfb148 304 ),
5589fc9c 305 child: Scaffold(
f0291f5a 306 backgroundColor: ProxmoxColors.supportBlue,
ab9e420b
TM
307 extendBodyBehindAppBar: true,
308 appBar: AppBar(
309 elevation: 0.0,
310 backgroundColor: Colors.transparent,
311 leading: IconButton(
7c043135 312 icon: const Icon(Icons.close),
ab9e420b
TM
313 onPressed: () => Navigator.of(context).pop(),
314 ),
315 ),
5589fc9c 316 body: Stack(
0e468546
TM
317 children: [
318 SingleChildScrollView(
319 child: ConstrainedBox(
320 constraints: BoxConstraints.tightFor(
321 height: MediaQuery.of(context).size.height),
959cc102
AL
322 child: SafeArea(
323 child: Padding(
324 padding: const EdgeInsets.all(8.0),
325 child: FutureBuilder<List<PveAccessDomainModel?>?>(
326 future: _accessDomains,
327 builder: (context, snapshot) {
328 return Form(
329 key: _formKey,
330 onChanged: () {
331 setState(() {
332 _submittButtonEnabled =
333 _formKey.currentState!.validate();
334 });
335 },
336 child: Column(
337 mainAxisAlignment: MainAxisAlignment.center,
338 children: [
339 Expanded(
340 child: Container(
341 child: Column(
342 mainAxisAlignment:
343 MainAxisAlignment.center,
344 children: [
345 Image.asset(
346 'assets/images/proxmox_logo_symbol_wordmark.png',
347 package: 'proxmox_login_manager',
348 ),
349 ],
350 ),
0e468546
TM
351 ),
352 ),
959cc102
AL
353 ProxmoxLoginForm(
354 originController: _originController,
355 originValidator: (value) {
356 if (value == null || value.isEmpty) {
357 return 'Please enter origin';
358 }
959cc102 359 try {
e2515b17 360 normalizeUrl(value);
959cc102
AL
361 return null;
362 } on FormatException catch (_) {
363 return 'Invalid URI';
e2515b17
DC
364 } on Exception catch (e) {
365 return 'Invalid URI: $e';
959cc102
AL
366 }
367 },
368 usernameController: _usernameController,
369 passwordController: _passwordController,
370 accessDomains: snapshot.data,
371 selectedDomain: _selectedDomain,
62145778
DC
372 onSavePasswordChanged: (value) {
373 _savePasswordCB = value;
374 },
375 canSavePassword: _canSavePassword,
376 passwordSaved: widget.password != null,
959cc102
AL
377 onDomainChanged: (value) {
378 setState(() {
379 _selectedDomain = value;
380 });
381 },
06ebbd20
TL
382 onOriginSubmitted: () {
383 final isValid =
384 _formKey.currentState!.validate();
385 setState(() {
386 _submittButtonEnabled = isValid;
387 });
388 if (isValid) {
389 setState(() {
390 _accessDomains = _getAccessDomains();
391 });
392 }
393 },
959cc102
AL
394 onPasswordSubmitted: _submittButtonEnabled
395 ? () {
396 final isValid =
397 _formKey.currentState!.validate();
398 setState(() {
399 _submittButtonEnabled = isValid;
400 });
401 if (isValid) {
402 _onLoginButtonPressed();
403 }
c2ebed05 404 }
959cc102
AL
405 : null,
406 ),
a8758f87
DC
407 Expanded(
408 child: Align(
409 alignment: Alignment.bottomCenter,
410 child: Container(
411 width: MediaQuery.of(context).size.width,
412 child: TextButton(
413 onPressed: _submittButtonEnabled
414 ? () {
415 final isValid = _formKey
416 .currentState!
417 .validate();
418 setState(() {
419 _submittButtonEnabled =
420 isValid;
421 });
422 if (isValid) {
423 if (snapshot.hasData) {
959cc102 424 _onLoginButtonPressed();
a8758f87 425 } else {
959cc102
AL
426 setState(() {
427 _accessDomains =
428 _getAccessDomains();
429 });
430 }
0e468546 431 }
a8758f87
DC
432 }
433 : null,
7c043135 434 child: const Text('Continue'),
0e468546
TM
435 ),
436 ),
437 ),
a8758f87 438 ),
959cc102
AL
439 ],
440 ),
441 );
442 }),
443 ),
0e468546
TM
444 ),
445 ),
446 ),
cff301db 447 if (_progressModel.inProgress > 0)
0e468546
TM
448 ProxmoxProgressOverlay(message: _progressModel.message),
449 ],
450 ),
451 ),
452 );
453 }
454
1dfe0c76 455 Future<void> _onLoginButtonPressed(
a9d1ee22 456 {String ticket = '', String? mRealm}) async {
0e468546
TM
457 setState(() {
458 _progressModel
cff301db 459 ..inProgress += 1
0e468546
TM
460 ..message = 'Authenticating...';
461 });
462
463 try {
464 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
3752cb66 465 //cleaned form fields
e2515b17 466 final origin = normalizeUrl(_originController.text.trim());
3752cb66 467 final username = _usernameController.text.trim();
62145778
DC
468 final String enteredPassword = _passwordController.text.trim();
469 final String? savedPassword = widget.password;
470
471 final password = ticket.isNotEmpty ? ticket : enteredPassword;
1dfe0c76 472 final realm = _selectedDomain?.realm ?? mRealm;
3752cb66 473
0e468546 474 var client = await proxclient.authenticate(
a9d1ee22 475 '$username@$realm', password, origin, settings.sslValidation!);
0e468546 476
15d7e1f8 477 if (client.credentials.tfa != null &&
7824e4fc 478 client.credentials.tfa!.kinds().isNotEmpty) {
02c15546
DC
479 ProxmoxApiClient? tfaclient =
480 await Navigator.of(context).push(MaterialPageRoute(
0e468546
TM
481 builder: (context) => ProxmoxTfaForm(
482 apiClient: client,
483 ),
df1d14b3 484 ));
02c15546
DC
485
486 if (tfaclient != null) {
487 client = tfaclient;
488 } else {
489 setState(() {
490 _progressModel.inProgress -= 1;
491 });
492 return;
493 }
0e468546
TM
494 }
495
1dfe0c76
TM
496 final status = await client.getClusterStatus();
497 final hostname =
f0291f5a 498 status.singleWhereOrNull((element) => element.local ?? false)?.name;
0e468546
TM
499 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
500
62145778
DC
501 final savePW = enteredPassword != '' &&
502 _savePasswordCB &&
503 enteredPassword != savedPassword;
504 final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB;
505 String? id;
506
a9d1ee22 507 if (widget.isCreate!) {
0e468546
TM
508 final newLogin = ProxmoxLoginModel((b) => b
509 ..origin = origin
3752cb66
TM
510 ..username = username
511 ..realm = realm
0e468546 512 ..productType = ProxmoxProductType.pve
1dfe0c76 513 ..ticket = client.credentials.ticket
62145778 514 ..passwordSaved = savePW
1dfe0c76 515 ..hostname = hostname);
3752cb66 516
a9d1ee22 517 loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin));
62145778 518 id = newLogin.identifier;
0e468546 519 } else {
a9d1ee22 520 loginStorage = loginStorage!.rebuild((b) => b
1dfe0c76
TM
521 ..logins.rebuildWhere(
522 (m) => m == widget.userModel,
523 (b) => b
524 ..ticket = client.credentials.ticket
62145778
DC
525 ..passwordSaved =
526 savePW || (deletePW ? false : b.passwordSaved ?? false)
1dfe0c76 527 ..hostname = hostname));
62145778
DC
528 id = widget.userModel!.identifier;
529 }
530
531 if (id != null) {
532 if (savePW) {
533 await savePassword(id, enteredPassword);
534 } else if (deletePW) {
535 await deletePassword(id);
536 }
0e468546
TM
537 }
538 await loginStorage.saveToDisk();
539
540 Navigator.of(context).pop(client);
541 } on proxclient.ProxmoxApiException catch (e) {
542 print(e);
f6cf3349
TM
543 if (e.message.contains('No ticket')) {
544 showDialog(
545 context: context,
546 builder: (context) => AlertDialog(
7c043135
TL
547 title: const Text('Version Error'),
548 content: const Text(
f6cf3349
TM
549 'Proxmox VE version not supported, please update your instance to use this app.'),
550 actions: [
fcdfb148 551 TextButton(
f6cf3349 552 onPressed: () => Navigator.of(context).pop(),
7c043135 553 child: const Text('Close'),
f6cf3349
TM
554 ),
555 ],
556 ),
557 );
558 } else {
559 showDialog(
560 context: context,
561 builder: (context) => ProxmoxApiErrorDialog(
562 exception: e,
563 ),
564 );
565 }
0e468546
TM
566 } catch (e, trace) {
567 print(e);
568 print(trace);
569 if (e.runtimeType == HandshakeException) {
570 showDialog(
571 context: context,
7c043135 572 builder: (context) => const ProxmoxCertificateErrorDialog(),
0e468546 573 );
f591bbbe
DC
574 } else {
575 showDialog(
576 context: context,
90a3b765 577 builder: (context) => ConnectionErrorDialog(exception: e),
f591bbbe 578 );
0e468546
TM
579 }
580 }
581 setState(() {
cff301db 582 _progressModel.inProgress -= 1;
0e468546
TM
583 });
584 }
585
9e6eb549 586 Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async {
f591bbbe
DC
587 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
588 List<PveAccessDomainModel?>? response;
589 try {
590 response = await proxclient.accessDomains(uri, settings.sslValidation!);
591 } on proxclient.ProxmoxApiException catch (e) {
592 showDialog(
593 context: context,
594 builder: (context) => ProxmoxApiErrorDialog(
595 exception: e,
596 ),
597 );
9e6eb549
DC
598 } on HandshakeException {
599 showDialog(
600 context: context,
7c043135 601 builder: (context) => const ProxmoxCertificateErrorDialog(),
9e6eb549
DC
602 );
603 }
604 return response;
605 }
606
607 Future<List<PveAccessDomainModel?>?> _tryLoadAccessDomains(Uri uri) async {
608 List<PveAccessDomainModel?>? response;
609 try {
610 response = await _loadAccessDomains(uri);
611 } catch (e) {
612 showDialog(
613 context: context,
614 builder: (context) => ConnectionErrorDialog(
615 exception: e,
616 ),
617 );
f591bbbe
DC
618 }
619 return response;
620 }
621
a9d1ee22 622 Future<List<PveAccessDomainModel?>?> _getAccessDomains() async {
0e468546
TM
623 setState(() {
624 _progressModel
cff301db 625 ..inProgress += 1
4f1019e4 626 ..message = 'Connecting...';
0e468546 627 });
62145778 628
c9fc855f 629 final canSavePW = await canSavePassword();
62145778 630 setState(() {
c9fc855f 631 _canSavePassword = canSavePW;
62145778
DC
632 });
633
59ab8956 634 var host = _originController.text.trim();
e2515b17 635 var apiBaseUrl = normalizeUrl(host);
0e468546 636
59ab8956
TL
637 RegExp portRE = new RegExp(r":\d{1,5}$");
638
a9d1ee22 639 List<PveAccessDomainModel?>? response;
f591bbbe 640
9e6eb549
DC
641 if (portRE.hasMatch(host)) {
642 response = await _tryLoadAccessDomains(apiBaseUrl);
643 } else {
644 // try to guess the port, 8006 first, and then 443
645 apiBaseUrl = apiBaseUrl.replace(port: 8006);
646 try {
647 response = await _loadAccessDomains(apiBaseUrl);
648 if (response != null) {
649 _originController.text = '$host:8006';
650 }
651 } catch (e) {
f591bbbe
DC
652 // we were no port given, and we couldn't reach on port 8006, retry with 443
653 apiBaseUrl = apiBaseUrl.replace(port: 443);
9e6eb549 654 response = await _tryLoadAccessDomains(apiBaseUrl);
a9088853
AL
655 if (response != null) {
656 _originController.text = '$host:443';
657 }
0e468546
TM
658 }
659 }
3752cb66 660
f0291f5a 661 response?.sort((a, b) => a!.realm.compareTo(b!.realm));
3752cb66 662
0e468546 663 final selection = response?.singleWhere(
a9d1ee22 664 (e) => e!.realm == widget.userModel?.realm,
0e468546
TM
665 orElse: () => response?.first,
666 );
3752cb66 667
0e468546 668 setState(() {
cff301db 669 _progressModel.inProgress -= 1;
0e468546
TM
670 _selectedDomain = selection;
671 });
3752cb66 672
0e468546
TM
673 return response;
674 }
675
676 @override
677 void dispose() {
678 _originController.dispose();
679 _usernameController.dispose();
680 _passwordController.dispose();
681 super.dispose();
682 }
683}
684
685class ProxmoxProgressOverlay extends StatelessWidget {
686 const ProxmoxProgressOverlay({
a9d1ee22
TL
687 Key? key,
688 required this.message,
0e468546
TM
689 }) : super(key: key);
690
691 final String message;
692
693 @override
694 Widget build(BuildContext context) {
695 return Container(
696 decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
697 child: Center(
698 child: Column(
699 mainAxisAlignment: MainAxisAlignment.center,
700 children: [
701 Text(
702 message,
7c043135 703 style: const TextStyle(
0e468546
TM
704 fontSize: 20,
705 ),
706 ),
7c043135
TL
707 const Padding(
708 padding: EdgeInsets.only(top: 20.0),
0e468546
TM
709 child: CircularProgressIndicator(),
710 )
711 ],
712 ),
713 ),
714 );
715 }
716}
717
90a3b765
DC
718class ConnectionErrorDialog extends StatelessWidget {
719 final exception;
720
721 const ConnectionErrorDialog({
722 Key? key,
723 required this.exception,
724 }) : super(key: key);
725
726 @override
727 Widget build(BuildContext context) {
728 return AlertDialog(
7c043135 729 title: const Text('Connection error'),
217eb475 730 content: Text('Could not establish connection: $exception'),
90a3b765
DC
731 actions: [
732 TextButton(
733 onPressed: () => Navigator.of(context).pop(),
7c043135 734 child: const Text('Close'),
90a3b765
DC
735 ),
736 ],
737 );
738 }
739}
740
0e468546
TM
741class ProxmoxApiErrorDialog extends StatelessWidget {
742 final proxclient.ProxmoxApiException exception;
743
744 const ProxmoxApiErrorDialog({
a9d1ee22
TL
745 Key? key,
746 required this.exception,
0e468546
TM
747 }) : super(key: key);
748
749 @override
750 Widget build(BuildContext context) {
751 return AlertDialog(
7c043135 752 title: const Text('API Error'),
0e468546
TM
753 content: SingleChildScrollView(
754 child: Text(exception.message),
755 ),
756 actions: [
fcdfb148 757 TextButton(
0e468546 758 onPressed: () => Navigator.of(context).pop(),
7c043135 759 child: const Text('Close'),
0e468546
TM
760 ),
761 ],
762 );
763 }
764}
765
766class ProxmoxCertificateErrorDialog extends StatelessWidget {
767 const ProxmoxCertificateErrorDialog({
a9d1ee22 768 Key? key,
0e468546
TM
769 }) : super(key: key);
770
771 @override
772 Widget build(BuildContext context) {
773 return AlertDialog(
7c043135 774 title: const Text('Certificate error'),
0e468546
TM
775 content: SingleChildScrollView(
776 child: Column(
777 crossAxisAlignment: CrossAxisAlignment.start,
778 children: [
7c043135 779 const Text('Your connection is not private.'),
0e468546
TM
780 Text(
781 'Note: Consider to disable SSL validation,'
782 ' if you use a self signed, not commonly trusted, certificate.',
f37413f7 783 style: Theme.of(context).textTheme.bodySmall,
0e468546
TM
784 ),
785 ],
786 ),
787 ),
788 actions: [
fcdfb148 789 TextButton(
0e468546 790 onPressed: () => Navigator.of(context).pop(),
7c043135 791 child: const Text('Close'),
0e468546 792 ),
fcdfb148 793 TextButton(
0e468546
TM
794 onPressed: () => Navigator.of(context).pushReplacement(
795 MaterialPageRoute(
796 builder: (context) => ProxmoxGeneralSettingsForm())),
7c043135 797 child: const Text('Settings'),
0e468546
TM
798 )
799 ],
800 );
801 }
802}
e2515b17
DC
803
804Uri normalizeUrl(String urlText) {
805 if (urlText.startsWith('https://')) {
806 urlText = urlText.substring('https://'.length);
807 }
808 if (urlText.startsWith('http://')) {
809 throw new Exception("HTTP without TLS is not supported");
810 }
811
812 return Uri.https(urlText, '');
813}