]> git.proxmox.com Git - flutter/proxmox_login_manager.git/blame - lib/proxmox_login_form.dart
tree wide: avoid using private types in public APIs for State
[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
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(
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,
98bcbcae 410 child: SizedBox(
a8758f87
DC
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) {
10b47afc 479 if (!mounted) return;
02c15546
DC
480 ProxmoxApiClient? tfaclient =
481 await Navigator.of(context).push(MaterialPageRoute(
0e468546
TM
482 builder: (context) => ProxmoxTfaForm(
483 apiClient: client,
484 ),
df1d14b3 485 ));
02c15546
DC
486
487 if (tfaclient != null) {
488 client = tfaclient;
489 } else {
490 setState(() {
491 _progressModel.inProgress -= 1;
492 });
493 return;
494 }
0e468546
TM
495 }
496
1dfe0c76
TM
497 final status = await client.getClusterStatus();
498 final hostname =
f0291f5a 499 status.singleWhereOrNull((element) => element.local ?? false)?.name;
0e468546
TM
500 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
501
62145778
DC
502 final savePW = enteredPassword != '' &&
503 _savePasswordCB &&
504 enteredPassword != savedPassword;
505 final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB;
506 String? id;
507
a9d1ee22 508 if (widget.isCreate!) {
0e468546
TM
509 final newLogin = ProxmoxLoginModel((b) => b
510 ..origin = origin
3752cb66
TM
511 ..username = username
512 ..realm = realm
0e468546 513 ..productType = ProxmoxProductType.pve
1dfe0c76 514 ..ticket = client.credentials.ticket
62145778 515 ..passwordSaved = savePW
1dfe0c76 516 ..hostname = hostname);
3752cb66 517
a9d1ee22 518 loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin));
62145778 519 id = newLogin.identifier;
0e468546 520 } else {
a9d1ee22 521 loginStorage = loginStorage!.rebuild((b) => b
1dfe0c76
TM
522 ..logins.rebuildWhere(
523 (m) => m == widget.userModel,
524 (b) => b
525 ..ticket = client.credentials.ticket
62145778
DC
526 ..passwordSaved =
527 savePW || (deletePW ? false : b.passwordSaved ?? false)
1dfe0c76 528 ..hostname = hostname));
62145778
DC
529 id = widget.userModel!.identifier;
530 }
531
532 if (id != null) {
533 if (savePW) {
534 await savePassword(id, enteredPassword);
535 } else if (deletePW) {
536 await deletePassword(id);
537 }
0e468546
TM
538 }
539 await loginStorage.saveToDisk();
540
10b47afc
TL
541 if (mounted) {
542 Navigator.of(context).pop(client);
543 }
0e468546
TM
544 } on proxclient.ProxmoxApiException catch (e) {
545 print(e);
10b47afc 546 if (!mounted) return;
f6cf3349
TM
547 if (e.message.contains('No ticket')) {
548 showDialog(
549 context: context,
550 builder: (context) => AlertDialog(
7c043135
TL
551 title: const Text('Version Error'),
552 content: const Text(
f6cf3349
TM
553 'Proxmox VE version not supported, please update your instance to use this app.'),
554 actions: [
fcdfb148 555 TextButton(
f6cf3349 556 onPressed: () => Navigator.of(context).pop(),
7c043135 557 child: const Text('Close'),
f6cf3349
TM
558 ),
559 ],
560 ),
561 );
562 } else {
563 showDialog(
564 context: context,
565 builder: (context) => ProxmoxApiErrorDialog(
566 exception: e,
567 ),
568 );
569 }
0e468546
TM
570 } catch (e, trace) {
571 print(e);
572 print(trace);
10b47afc
TL
573 if (mounted) {
574 if (e.runtimeType == HandshakeException) {
575 showDialog(
576 context: context,
577 builder: (context) => const ProxmoxCertificateErrorDialog(),
578 );
579 } else {
580 showDialog(
581 context: context,
582 builder: (context) => ConnectionErrorDialog(exception: e),
583 );
584 }
0e468546
TM
585 }
586 }
587 setState(() {
cff301db 588 _progressModel.inProgress -= 1;
0e468546
TM
589 });
590 }
591
9e6eb549 592 Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async {
f591bbbe
DC
593 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
594 List<PveAccessDomainModel?>? response;
595 try {
596 response = await proxclient.accessDomains(uri, settings.sslValidation!);
597 } on proxclient.ProxmoxApiException catch (e) {
10b47afc
TL
598 if (mounted) {
599 showDialog(
600 context: context,
601 builder: (context) => ProxmoxApiErrorDialog(
602 exception: e,
603 ),
604 );
605 }
9e6eb549 606 } on HandshakeException {
10b47afc
TL
607 if (mounted) {
608 showDialog(
609 context: context,
610 builder: (context) => const ProxmoxCertificateErrorDialog(),
611 );
612 }
9e6eb549
DC
613 }
614 return response;
615 }
616
617 Future<List<PveAccessDomainModel?>?> _tryLoadAccessDomains(Uri uri) async {
618 List<PveAccessDomainModel?>? response;
619 try {
620 response = await _loadAccessDomains(uri);
621 } catch (e) {
10b47afc
TL
622 if (mounted) {
623 showDialog(
624 context: context,
625 builder: (context) => ConnectionErrorDialog(
626 exception: e,
627 ),
628 );
629 }
f591bbbe
DC
630 }
631 return response;
632 }
633
a9d1ee22 634 Future<List<PveAccessDomainModel?>?> _getAccessDomains() async {
0e468546
TM
635 setState(() {
636 _progressModel
cff301db 637 ..inProgress += 1
4f1019e4 638 ..message = 'Connecting...';
0e468546 639 });
62145778 640
c9fc855f 641 final canSavePW = await canSavePassword();
62145778 642 setState(() {
c9fc855f 643 _canSavePassword = canSavePW;
62145778
DC
644 });
645
59ab8956 646 var host = _originController.text.trim();
e2515b17 647 var apiBaseUrl = normalizeUrl(host);
0e468546 648
31126444 649 RegExp portRE = RegExp(r":\d{1,5}$");
59ab8956 650
a9d1ee22 651 List<PveAccessDomainModel?>? response;
f591bbbe 652
9e6eb549
DC
653 if (portRE.hasMatch(host)) {
654 response = await _tryLoadAccessDomains(apiBaseUrl);
655 } else {
656 // try to guess the port, 8006 first, and then 443
657 apiBaseUrl = apiBaseUrl.replace(port: 8006);
658 try {
659 response = await _loadAccessDomains(apiBaseUrl);
660 if (response != null) {
661 _originController.text = '$host:8006';
662 }
663 } catch (e) {
f591bbbe
DC
664 // we were no port given, and we couldn't reach on port 8006, retry with 443
665 apiBaseUrl = apiBaseUrl.replace(port: 443);
9e6eb549 666 response = await _tryLoadAccessDomains(apiBaseUrl);
a9088853
AL
667 if (response != null) {
668 _originController.text = '$host:443';
669 }
0e468546
TM
670 }
671 }
3752cb66 672
f0291f5a 673 response?.sort((a, b) => a!.realm.compareTo(b!.realm));
3752cb66 674
0e468546 675 final selection = response?.singleWhere(
a9d1ee22 676 (e) => e!.realm == widget.userModel?.realm,
0e468546
TM
677 orElse: () => response?.first,
678 );
3752cb66 679
0e468546 680 setState(() {
cff301db 681 _progressModel.inProgress -= 1;
0e468546
TM
682 _selectedDomain = selection;
683 });
3752cb66 684
0e468546
TM
685 return response;
686 }
687
688 @override
689 void dispose() {
690 _originController.dispose();
691 _usernameController.dispose();
692 _passwordController.dispose();
693 super.dispose();
694 }
695}
696
697class ProxmoxProgressOverlay extends StatelessWidget {
698 const ProxmoxProgressOverlay({
a9d1ee22
TL
699 Key? key,
700 required this.message,
0e468546
TM
701 }) : super(key: key);
702
703 final String message;
704
705 @override
706 Widget build(BuildContext context) {
707 return Container(
31126444 708 decoration: BoxDecoration(color: Colors.black.withOpacity(0.5)),
0e468546
TM
709 child: Center(
710 child: Column(
711 mainAxisAlignment: MainAxisAlignment.center,
712 children: [
713 Text(
714 message,
7c043135 715 style: const TextStyle(
0e468546
TM
716 fontSize: 20,
717 ),
718 ),
7c043135
TL
719 const Padding(
720 padding: EdgeInsets.only(top: 20.0),
0e468546
TM
721 child: CircularProgressIndicator(),
722 )
723 ],
724 ),
725 ),
726 );
727 }
728}
729
90a3b765 730class ConnectionErrorDialog extends StatelessWidget {
f82ea945 731 final Object exception;
90a3b765
DC
732
733 const ConnectionErrorDialog({
734 Key? key,
735 required this.exception,
736 }) : super(key: key);
737
738 @override
739 Widget build(BuildContext context) {
740 return AlertDialog(
7c043135 741 title: const Text('Connection error'),
217eb475 742 content: Text('Could not establish connection: $exception'),
90a3b765
DC
743 actions: [
744 TextButton(
745 onPressed: () => Navigator.of(context).pop(),
7c043135 746 child: const Text('Close'),
90a3b765
DC
747 ),
748 ],
749 );
750 }
751}
752
0e468546
TM
753class ProxmoxApiErrorDialog extends StatelessWidget {
754 final proxclient.ProxmoxApiException exception;
755
756 const ProxmoxApiErrorDialog({
a9d1ee22
TL
757 Key? key,
758 required this.exception,
0e468546
TM
759 }) : super(key: key);
760
761 @override
762 Widget build(BuildContext context) {
763 return AlertDialog(
7c043135 764 title: const Text('API Error'),
0e468546
TM
765 content: SingleChildScrollView(
766 child: Text(exception.message),
767 ),
768 actions: [
fcdfb148 769 TextButton(
0e468546 770 onPressed: () => Navigator.of(context).pop(),
7c043135 771 child: const Text('Close'),
0e468546
TM
772 ),
773 ],
774 );
775 }
776}
777
778class ProxmoxCertificateErrorDialog extends StatelessWidget {
779 const ProxmoxCertificateErrorDialog({
a9d1ee22 780 Key? key,
0e468546
TM
781 }) : super(key: key);
782
783 @override
784 Widget build(BuildContext context) {
785 return AlertDialog(
7c043135 786 title: const Text('Certificate error'),
0e468546
TM
787 content: SingleChildScrollView(
788 child: Column(
789 crossAxisAlignment: CrossAxisAlignment.start,
790 children: [
7c043135 791 const Text('Your connection is not private.'),
0e468546
TM
792 Text(
793 'Note: Consider to disable SSL validation,'
794 ' if you use a self signed, not commonly trusted, certificate.',
f37413f7 795 style: Theme.of(context).textTheme.bodySmall,
0e468546
TM
796 ),
797 ],
798 ),
799 ),
800 actions: [
fcdfb148 801 TextButton(
0e468546 802 onPressed: () => Navigator.of(context).pop(),
7c043135 803 child: const Text('Close'),
0e468546 804 ),
fcdfb148 805 TextButton(
0e468546
TM
806 onPressed: () => Navigator.of(context).pushReplacement(
807 MaterialPageRoute(
264bf286 808 builder: (context) => const ProxmoxGeneralSettingsForm())),
7c043135 809 child: const Text('Settings'),
0e468546
TM
810 )
811 ],
812 );
813 }
814}
e2515b17
DC
815
816Uri normalizeUrl(String urlText) {
817 if (urlText.startsWith('https://')) {
818 urlText = urlText.substring('https://'.length);
819 }
820 if (urlText.startsWith('http://')) {
31126444 821 throw Exception("HTTP without TLS is not supported");
e2515b17
DC
822 }
823
824 return Uri.https(urlText, '');
825}