]> git.proxmox.com Git - flutter/proxmox_login_manager.git/blame - lib/proxmox_login_form.dart
fix import of collection package
[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';
8a1de1b4 5import 'package:collection/collection.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
f82ea945 63 State<ProxmoxLoginForm> createState() => _ProxmoxLoginFormState();
0e468546
TM
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
f82ea945 203 State<ProxmoxLoginPage> createState() => _ProxmoxLoginPageState();
0e468546
TM
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(
f09d9506
TL
340 child: Column(
341 mainAxisAlignment: MainAxisAlignment.center,
342 children: [
343 Image.asset(
344 'assets/images/proxmox_logo_symbol_wordmark.png',
345 package: 'proxmox_login_manager',
346 ),
347 ],
0e468546
TM
348 ),
349 ),
959cc102
AL
350 ProxmoxLoginForm(
351 originController: _originController,
352 originValidator: (value) {
353 if (value == null || value.isEmpty) {
354 return 'Please enter origin';
355 }
959cc102 356 try {
e2515b17 357 normalizeUrl(value);
959cc102
AL
358 return null;
359 } on FormatException catch (_) {
360 return 'Invalid URI';
e2515b17
DC
361 } on Exception catch (e) {
362 return 'Invalid URI: $e';
959cc102
AL
363 }
364 },
365 usernameController: _usernameController,
366 passwordController: _passwordController,
367 accessDomains: snapshot.data,
368 selectedDomain: _selectedDomain,
62145778
DC
369 onSavePasswordChanged: (value) {
370 _savePasswordCB = value;
371 },
372 canSavePassword: _canSavePassword,
373 passwordSaved: widget.password != null,
959cc102
AL
374 onDomainChanged: (value) {
375 setState(() {
376 _selectedDomain = value;
377 });
378 },
06ebbd20
TL
379 onOriginSubmitted: () {
380 final isValid =
381 _formKey.currentState!.validate();
382 setState(() {
383 _submittButtonEnabled = isValid;
384 });
385 if (isValid) {
386 setState(() {
387 _accessDomains = _getAccessDomains();
388 });
389 }
390 },
959cc102
AL
391 onPasswordSubmitted: _submittButtonEnabled
392 ? () {
393 final isValid =
394 _formKey.currentState!.validate();
395 setState(() {
396 _submittButtonEnabled = isValid;
397 });
398 if (isValid) {
399 _onLoginButtonPressed();
400 }
c2ebed05 401 }
959cc102
AL
402 : null,
403 ),
a8758f87
DC
404 Expanded(
405 child: Align(
406 alignment: Alignment.bottomCenter,
98bcbcae 407 child: SizedBox(
a8758f87
DC
408 width: MediaQuery.of(context).size.width,
409 child: TextButton(
410 onPressed: _submittButtonEnabled
411 ? () {
412 final isValid = _formKey
413 .currentState!
414 .validate();
415 setState(() {
416 _submittButtonEnabled =
417 isValid;
418 });
419 if (isValid) {
420 if (snapshot.hasData) {
959cc102 421 _onLoginButtonPressed();
a8758f87 422 } else {
959cc102
AL
423 setState(() {
424 _accessDomains =
425 _getAccessDomains();
426 });
427 }
0e468546 428 }
a8758f87
DC
429 }
430 : null,
7c043135 431 child: const Text('Continue'),
0e468546
TM
432 ),
433 ),
434 ),
a8758f87 435 ),
959cc102
AL
436 ],
437 ),
438 );
439 }),
440 ),
0e468546
TM
441 ),
442 ),
443 ),
cff301db 444 if (_progressModel.inProgress > 0)
0e468546
TM
445 ProxmoxProgressOverlay(message: _progressModel.message),
446 ],
447 ),
448 ),
449 );
450 }
451
1dfe0c76 452 Future<void> _onLoginButtonPressed(
a9d1ee22 453 {String ticket = '', String? mRealm}) async {
0e468546
TM
454 setState(() {
455 _progressModel
cff301db 456 ..inProgress += 1
0e468546
TM
457 ..message = 'Authenticating...';
458 });
459
460 try {
461 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
3752cb66 462 //cleaned form fields
e2515b17 463 final origin = normalizeUrl(_originController.text.trim());
3752cb66 464 final username = _usernameController.text.trim();
62145778
DC
465 final String enteredPassword = _passwordController.text.trim();
466 final String? savedPassword = widget.password;
467
468 final password = ticket.isNotEmpty ? ticket : enteredPassword;
1dfe0c76 469 final realm = _selectedDomain?.realm ?? mRealm;
3752cb66 470
0e468546 471 var client = await proxclient.authenticate(
a9d1ee22 472 '$username@$realm', password, origin, settings.sslValidation!);
0e468546 473
15d7e1f8 474 if (client.credentials.tfa != null &&
7824e4fc 475 client.credentials.tfa!.kinds().isNotEmpty) {
10b47afc 476 if (!mounted) return;
02c15546
DC
477 ProxmoxApiClient? tfaclient =
478 await Navigator.of(context).push(MaterialPageRoute(
0e468546
TM
479 builder: (context) => ProxmoxTfaForm(
480 apiClient: client,
481 ),
df1d14b3 482 ));
02c15546
DC
483
484 if (tfaclient != null) {
485 client = tfaclient;
486 } else {
487 setState(() {
488 _progressModel.inProgress -= 1;
489 });
490 return;
491 }
0e468546
TM
492 }
493
1dfe0c76
TM
494 final status = await client.getClusterStatus();
495 final hostname =
f0291f5a 496 status.singleWhereOrNull((element) => element.local ?? false)?.name;
0e468546
TM
497 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
498
62145778
DC
499 final savePW = enteredPassword != '' &&
500 _savePasswordCB &&
501 enteredPassword != savedPassword;
502 final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB;
503 String? id;
504
a9d1ee22 505 if (widget.isCreate!) {
0e468546
TM
506 final newLogin = ProxmoxLoginModel((b) => b
507 ..origin = origin
3752cb66
TM
508 ..username = username
509 ..realm = realm
0e468546 510 ..productType = ProxmoxProductType.pve
1dfe0c76 511 ..ticket = client.credentials.ticket
62145778 512 ..passwordSaved = savePW
1dfe0c76 513 ..hostname = hostname);
3752cb66 514
a9d1ee22 515 loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin));
62145778 516 id = newLogin.identifier;
0e468546 517 } else {
a9d1ee22 518 loginStorage = loginStorage!.rebuild((b) => b
1dfe0c76
TM
519 ..logins.rebuildWhere(
520 (m) => m == widget.userModel,
521 (b) => b
522 ..ticket = client.credentials.ticket
62145778
DC
523 ..passwordSaved =
524 savePW || (deletePW ? false : b.passwordSaved ?? false)
1dfe0c76 525 ..hostname = hostname));
62145778
DC
526 id = widget.userModel!.identifier;
527 }
528
529 if (id != null) {
530 if (savePW) {
531 await savePassword(id, enteredPassword);
532 } else if (deletePW) {
533 await deletePassword(id);
534 }
0e468546
TM
535 }
536 await loginStorage.saveToDisk();
537
10b47afc
TL
538 if (mounted) {
539 Navigator.of(context).pop(client);
540 }
0e468546
TM
541 } on proxclient.ProxmoxApiException catch (e) {
542 print(e);
10b47afc 543 if (!mounted) return;
f6cf3349
TM
544 if (e.message.contains('No ticket')) {
545 showDialog(
546 context: context,
547 builder: (context) => AlertDialog(
7c043135
TL
548 title: const Text('Version Error'),
549 content: const Text(
f6cf3349
TM
550 'Proxmox VE version not supported, please update your instance to use this app.'),
551 actions: [
fcdfb148 552 TextButton(
f6cf3349 553 onPressed: () => Navigator.of(context).pop(),
7c043135 554 child: const Text('Close'),
f6cf3349
TM
555 ),
556 ],
557 ),
558 );
559 } else {
560 showDialog(
561 context: context,
562 builder: (context) => ProxmoxApiErrorDialog(
563 exception: e,
564 ),
565 );
566 }
0e468546
TM
567 } catch (e, trace) {
568 print(e);
569 print(trace);
10b47afc
TL
570 if (mounted) {
571 if (e.runtimeType == HandshakeException) {
572 showDialog(
573 context: context,
574 builder: (context) => const ProxmoxCertificateErrorDialog(),
575 );
576 } else {
577 showDialog(
578 context: context,
579 builder: (context) => ConnectionErrorDialog(exception: e),
580 );
581 }
0e468546
TM
582 }
583 }
584 setState(() {
cff301db 585 _progressModel.inProgress -= 1;
0e468546
TM
586 });
587 }
588
9e6eb549 589 Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async {
f591bbbe
DC
590 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
591 List<PveAccessDomainModel?>? response;
592 try {
593 response = await proxclient.accessDomains(uri, settings.sslValidation!);
594 } on proxclient.ProxmoxApiException catch (e) {
10b47afc
TL
595 if (mounted) {
596 showDialog(
597 context: context,
598 builder: (context) => ProxmoxApiErrorDialog(
599 exception: e,
600 ),
601 );
602 }
9e6eb549 603 } on HandshakeException {
10b47afc
TL
604 if (mounted) {
605 showDialog(
606 context: context,
607 builder: (context) => const ProxmoxCertificateErrorDialog(),
608 );
609 }
9e6eb549
DC
610 }
611 return response;
612 }
613
614 Future<List<PveAccessDomainModel?>?> _tryLoadAccessDomains(Uri uri) async {
615 List<PveAccessDomainModel?>? response;
616 try {
617 response = await _loadAccessDomains(uri);
618 } catch (e) {
10b47afc
TL
619 if (mounted) {
620 showDialog(
621 context: context,
622 builder: (context) => ConnectionErrorDialog(
623 exception: e,
624 ),
625 );
626 }
f591bbbe
DC
627 }
628 return response;
629 }
630
a9d1ee22 631 Future<List<PveAccessDomainModel?>?> _getAccessDomains() async {
0e468546
TM
632 setState(() {
633 _progressModel
cff301db 634 ..inProgress += 1
4f1019e4 635 ..message = 'Connecting...';
0e468546 636 });
62145778 637
c9fc855f 638 final canSavePW = await canSavePassword();
62145778 639 setState(() {
c9fc855f 640 _canSavePassword = canSavePW;
62145778
DC
641 });
642
59ab8956 643 var host = _originController.text.trim();
e2515b17 644 var apiBaseUrl = normalizeUrl(host);
0e468546 645
31126444 646 RegExp portRE = RegExp(r":\d{1,5}$");
59ab8956 647
a9d1ee22 648 List<PveAccessDomainModel?>? response;
f591bbbe 649
9e6eb549
DC
650 if (portRE.hasMatch(host)) {
651 response = await _tryLoadAccessDomains(apiBaseUrl);
652 } else {
653 // try to guess the port, 8006 first, and then 443
654 apiBaseUrl = apiBaseUrl.replace(port: 8006);
655 try {
656 response = await _loadAccessDomains(apiBaseUrl);
657 if (response != null) {
658 _originController.text = '$host:8006';
659 }
660 } catch (e) {
f591bbbe
DC
661 // we were no port given, and we couldn't reach on port 8006, retry with 443
662 apiBaseUrl = apiBaseUrl.replace(port: 443);
9e6eb549 663 response = await _tryLoadAccessDomains(apiBaseUrl);
a9088853
AL
664 if (response != null) {
665 _originController.text = '$host:443';
666 }
0e468546
TM
667 }
668 }
3752cb66 669
f0291f5a 670 response?.sort((a, b) => a!.realm.compareTo(b!.realm));
3752cb66 671
0e468546 672 final selection = response?.singleWhere(
a9d1ee22 673 (e) => e!.realm == widget.userModel?.realm,
0e468546
TM
674 orElse: () => response?.first,
675 );
3752cb66 676
0e468546 677 setState(() {
cff301db 678 _progressModel.inProgress -= 1;
0e468546
TM
679 _selectedDomain = selection;
680 });
3752cb66 681
0e468546
TM
682 return response;
683 }
684
685 @override
686 void dispose() {
687 _originController.dispose();
688 _usernameController.dispose();
689 _passwordController.dispose();
690 super.dispose();
691 }
692}
693
694class ProxmoxProgressOverlay extends StatelessWidget {
695 const ProxmoxProgressOverlay({
a9d1ee22
TL
696 Key? key,
697 required this.message,
0e468546
TM
698 }) : super(key: key);
699
700 final String message;
701
702 @override
703 Widget build(BuildContext context) {
704 return Container(
31126444 705 decoration: BoxDecoration(color: Colors.black.withOpacity(0.5)),
0e468546
TM
706 child: Center(
707 child: Column(
708 mainAxisAlignment: MainAxisAlignment.center,
709 children: [
710 Text(
711 message,
7c043135 712 style: const TextStyle(
0e468546
TM
713 fontSize: 20,
714 ),
715 ),
7c043135
TL
716 const Padding(
717 padding: EdgeInsets.only(top: 20.0),
0e468546
TM
718 child: CircularProgressIndicator(),
719 )
720 ],
721 ),
722 ),
723 );
724 }
725}
726
90a3b765 727class ConnectionErrorDialog extends StatelessWidget {
f82ea945 728 final Object exception;
90a3b765
DC
729
730 const ConnectionErrorDialog({
731 Key? key,
732 required this.exception,
733 }) : super(key: key);
734
735 @override
736 Widget build(BuildContext context) {
737 return AlertDialog(
7c043135 738 title: const Text('Connection error'),
217eb475 739 content: Text('Could not establish connection: $exception'),
90a3b765
DC
740 actions: [
741 TextButton(
742 onPressed: () => Navigator.of(context).pop(),
7c043135 743 child: const Text('Close'),
90a3b765
DC
744 ),
745 ],
746 );
747 }
748}
749
0e468546
TM
750class ProxmoxApiErrorDialog extends StatelessWidget {
751 final proxclient.ProxmoxApiException exception;
752
753 const ProxmoxApiErrorDialog({
a9d1ee22
TL
754 Key? key,
755 required this.exception,
0e468546
TM
756 }) : super(key: key);
757
758 @override
759 Widget build(BuildContext context) {
760 return AlertDialog(
7c043135 761 title: const Text('API Error'),
0e468546
TM
762 content: SingleChildScrollView(
763 child: Text(exception.message),
764 ),
765 actions: [
fcdfb148 766 TextButton(
0e468546 767 onPressed: () => Navigator.of(context).pop(),
7c043135 768 child: const Text('Close'),
0e468546
TM
769 ),
770 ],
771 );
772 }
773}
774
775class ProxmoxCertificateErrorDialog extends StatelessWidget {
776 const ProxmoxCertificateErrorDialog({
a9d1ee22 777 Key? key,
0e468546
TM
778 }) : super(key: key);
779
780 @override
781 Widget build(BuildContext context) {
782 return AlertDialog(
7c043135 783 title: const Text('Certificate error'),
0e468546
TM
784 content: SingleChildScrollView(
785 child: Column(
786 crossAxisAlignment: CrossAxisAlignment.start,
787 children: [
7c043135 788 const Text('Your connection is not private.'),
0e468546
TM
789 Text(
790 'Note: Consider to disable SSL validation,'
791 ' if you use a self signed, not commonly trusted, certificate.',
f37413f7 792 style: Theme.of(context).textTheme.bodySmall,
0e468546
TM
793 ),
794 ],
795 ),
796 ),
797 actions: [
fcdfb148 798 TextButton(
0e468546 799 onPressed: () => Navigator.of(context).pop(),
7c043135 800 child: const Text('Close'),
0e468546 801 ),
fcdfb148 802 TextButton(
0e468546
TM
803 onPressed: () => Navigator.of(context).pushReplacement(
804 MaterialPageRoute(
264bf286 805 builder: (context) => const ProxmoxGeneralSettingsForm())),
7c043135 806 child: const Text('Settings'),
0e468546
TM
807 )
808 ],
809 );
810 }
811}
e2515b17
DC
812
813Uri normalizeUrl(String urlText) {
814 if (urlText.startsWith('https://')) {
815 urlText = urlText.substring('https://'.length);
816 }
817 if (urlText.startsWith('http://')) {
31126444 818 throw Exception("HTTP without TLS is not supported");
e2515b17
DC
819 }
820
821 return Uri.https(urlText, '');
822}