import 'package:proxmox_login_manager/proxmox_login_model.dart';
import 'package:proxmox_login_manager/proxmox_tfa_form.dart';
import 'package:proxmox_login_manager/extension.dart';
+import 'package:proxmox_login_manager/proxmox_password_store.dart';
class ProxmoxProgressModel {
- bool inProgress = false;
+ int inProgress = 0;
String message = 'Loading...';
ProxmoxProgressModel({
- this.inProgress = false,
+ this.inProgress = 0,
this.message = 'Loading...',
});
}
// FIXME: copied from pve_flutter_frontend, re-use common set
class ProxmoxColors {
- static final Color orange = Color(0xFFE57000);
- static final Color supportGrey = Color(0xFFABBABA);
- static final Color supportBlue = Color(0xFF00617F);
+ static const Color orange = Color(0xFFE57000);
+ static const Color supportGrey = Color(0xFFABBABA);
+ static const Color supportBlue = Color(0xFF00617F);
}
class ProxmoxLoginForm extends StatefulWidget {
final PveAccessDomainModel? selectedDomain;
final ValueChanged<PveAccessDomainModel?> onDomainChanged;
final Function? onPasswordSubmitted;
- final Function? onOriginSubmitted;
+ final Function onOriginSubmitted;
+ final Function? onSavePasswordChanged;
+ final bool? canSavePassword;
+ final bool? passwordSaved;
const ProxmoxLoginForm({
Key? key,
this.selectedDomain,
required this.onDomainChanged,
this.onPasswordSubmitted,
- this.onOriginSubmitted,
+ required this.onOriginSubmitted,
+ this.onSavePasswordChanged,
+ this.canSavePassword,
+ this.passwordSaved,
}) : super(key: key);
@override
- _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState();
+ State<ProxmoxLoginForm> createState() => _ProxmoxLoginFormState();
}
class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
bool _obscure = true;
+ bool? _savePwCheckbox;
FocusNode? passwordFocusNode;
@override
Widget build(BuildContext context) {
if (widget.accessDomains == null) {
return TextFormField(
- decoration: InputDecoration(
+ decoration: const InputDecoration(
icon: Icon(Icons.vpn_lock),
labelText: 'Origin',
hintText: 'e.g. 192.168.1.2',
- helperText: 'Protocol (https) and default port (8006) implied'),
+ helperText:
+ 'Protocol (https) and default port (8006 or 443) implied'),
+ textInputAction: TextInputAction.next,
controller: widget.originController,
validator: widget.originValidator,
- onFieldSubmitted: (value) => widget.onOriginSubmitted!(),
+ onFieldSubmitted: (value) => widget.onOriginSubmitted(),
);
}
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
- decoration: InputDecoration(
+ decoration: const InputDecoration(
icon: Icon(Icons.vpn_lock),
labelText: 'Origin',
),
enabled: false,
),
TextFormField(
- decoration: InputDecoration(
+ decoration: const InputDecoration(
icon: Icon(Icons.person),
labelText: 'Username',
),
}
return null;
},
- autofillHints: [AutofillHints.username],
+ autofillHints: const [AutofillHints.username],
),
DropdownButtonFormField(
- decoration: InputDecoration(icon: Icon(Icons.domain)),
+ decoration: const InputDecoration(icon: Icon(Icons.domain)),
items: widget.accessDomains!
.map((e) => DropdownMenuItem(
+ value: e,
child: ListTile(
title: Text(e!.realm),
subtitle: Text(e.comment ?? ''),
),
- value: e,
))
.toList(),
onChanged: widget.onDomainChanged,
Stack(
children: [
TextFormField(
- decoration: InputDecoration(
+ decoration: const InputDecoration(
icon: Icon(Icons.lock),
labelText: 'Password',
),
return null;
},
onFieldSubmitted: (value) => widget.onPasswordSubmitted!(),
- autofillHints: [AutofillHints.password],
+ autofillHints: const [AutofillHints.password],
),
Align(
alignment: Alignment.bottomRight,
child: IconButton(
- constraints: BoxConstraints.tight(Size(58, 58)),
+ constraints: BoxConstraints.tight(const Size(58, 58)),
iconSize: 24,
icon:
Icon(_obscure ? Icons.visibility : Icons.visibility_off),
)
],
),
+ if (widget.canSavePassword ?? false)
+ CheckboxListTile(
+ title: const Text('Save password'),
+ value: _savePwCheckbox ?? widget.passwordSaved ?? false,
+ onChanged: (value) {
+ if (widget.onSavePasswordChanged != null) {
+ widget.onSavePasswordChanged!(value!);
+ }
+ setState(() {
+ _savePwCheckbox = value!;
+ });
+ },
+ )
],
),
);
final ProxmoxLoginModel? userModel;
final bool? isCreate;
final String? ticket;
+ final String? password;
const ProxmoxLoginPage({
Key? key,
this.userModel,
this.isCreate,
this.ticket = '',
+ this.password,
}) : super(key: key);
@override
- _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
+ State<ProxmoxLoginPage> createState() => _ProxmoxLoginPageState();
}
class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
final _formKey = GlobalKey<FormState>();
ProxmoxProgressModel _progressModel = ProxmoxProgressModel();
bool _submittButtonEnabled = true;
+ bool _canSavePassword = false;
+ bool _savePasswordCB = false;
@override
void initState() {
final userModel = widget.userModel;
_progressModel = ProxmoxProgressModel();
if (!widget.isCreate! && userModel != null) {
- _progressModel
- ..inProgress = true
- ..message = 'Connection test...';
- _originController.text =
- '${userModel.origin?.host}:${userModel.origin?.port}';
+ _originController.text = userModel.origin?.toString() ?? '';
+ // Uri does not append 443 for https, so we do it manually
+ if (userModel.origin != null &&
+ userModel.origin!.scheme == "https" &&
+ userModel.origin!.port == 443) {
+ _originController.text += ":443";
+ }
+ _passwordController.text = widget.password ?? '';
_accessDomains = _getAccessDomains();
_usernameController.text = userModel.username!;
- if (widget.ticket!.isNotEmpty && userModel.activeSession) {
+ _savePasswordCB = widget.password != null;
+ if ((widget.ticket!.isNotEmpty && userModel.activeSession) ||
+ widget.password != null) {
_onLoginButtonPressed(ticket: widget.ticket!, mRealm: userModel.realm);
}
}
return Theme(
//data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
data: ThemeData.dark().copyWith(
- colorScheme: ColorScheme.dark().copyWith(
- secondary: ProxmoxColors.orange,
- onSecondary: ProxmoxColors.supportGrey)),
+ textButtonTheme: TextButtonThemeData(
+ style: TextButton.styleFrom(
+ foregroundColor: Colors.white,
+ backgroundColor: ProxmoxColors.orange,
+ disabledBackgroundColor: Colors.grey,
+ ),
+ ),
+ colorScheme: const ColorScheme.dark().copyWith(
+ primary: ProxmoxColors.orange,
+ secondary: ProxmoxColors.orange,
+ onSecondary: ProxmoxColors.supportGrey),
+ checkboxTheme: CheckboxThemeData(
+ fillColor: MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return null;
+ }
+ if (states.contains(MaterialState.selected)) {
+ return ProxmoxColors.orange;
+ }
+ return null;
+ }),
+ ),
+ radioTheme: RadioThemeData(
+ fillColor: MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return null;
+ }
+ if (states.contains(MaterialState.selected)) {
+ return ProxmoxColors.orange;
+ }
+ return null;
+ }),
+ ),
+ switchTheme: SwitchThemeData(
+ thumbColor: MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return null;
+ }
+ if (states.contains(MaterialState.selected)) {
+ return ProxmoxColors.orange;
+ }
+ return null;
+ }),
+ trackColor: MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return null;
+ }
+ if (states.contains(MaterialState.selected)) {
+ return ProxmoxColors.orange;
+ }
+ return null;
+ }),
+ ),
+ ),
child: Scaffold(
backgroundColor: ProxmoxColors.supportBlue,
extendBodyBehindAppBar: true,
elevation: 0.0,
backgroundColor: Colors.transparent,
leading: IconButton(
- icon: Icon(Icons.close),
+ icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
),
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(
height: MediaQuery.of(context).size.height),
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: FutureBuilder<List<PveAccessDomainModel?>?>(
- future: _accessDomains,
- builder: (context, snapshot) {
- return Form(
- key: _formKey,
- onChanged: () {
- setState(() {
- _submittButtonEnabled =
- _formKey.currentState!.validate();
- });
- },
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Expanded(
- child: Container(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Image.asset(
- 'assets/images/proxmox_logo_symbol_wordmark.png',
- package: 'proxmox_login_manager',
- ),
- ],
+ child: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: FutureBuilder<List<PveAccessDomainModel?>?>(
+ future: _accessDomains,
+ builder: (context, snapshot) {
+ return Form(
+ key: _formKey,
+ onChanged: () {
+ setState(() {
+ _submittButtonEnabled =
+ _formKey.currentState!.validate();
+ });
+ },
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Expanded(
+ child: Container(
+ child: Column(
+ mainAxisAlignment:
+ MainAxisAlignment.center,
+ children: [
+ Image.asset(
+ 'assets/images/proxmox_logo_symbol_wordmark.png',
+ package: 'proxmox_login_manager',
+ ),
+ ],
+ ),
),
),
- ),
- ProxmoxLoginForm(
- originController: _originController,
- originValidator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter origin';
- }
- if (value.startsWith('https://') ||
- value.startsWith('http://')) {
- return 'Do not prefix with scheme';
- }
- try {
- Uri.https(value, '');
- return null;
- } on FormatException catch (_) {
- return 'Invalid URI';
- }
- },
- usernameController: _usernameController,
- passwordController: _passwordController,
- accessDomains: snapshot.data,
- selectedDomain: _selectedDomain,
- onDomainChanged: (value) {
- setState(() {
- _selectedDomain = value;
- });
- },
- onOriginSubmitted: _submittButtonEnabled
- ? () {
- final isValid =
- _formKey.currentState!.validate();
- setState(() {
- _submittButtonEnabled = isValid;
- });
- if (isValid) {
+ ProxmoxLoginForm(
+ originController: _originController,
+ originValidator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter origin';
+ }
+ try {
+ normalizeUrl(value);
+ return null;
+ } on FormatException catch (_) {
+ return 'Invalid URI';
+ } on Exception catch (e) {
+ return 'Invalid URI: $e';
+ }
+ },
+ usernameController: _usernameController,
+ passwordController: _passwordController,
+ accessDomains: snapshot.data,
+ selectedDomain: _selectedDomain,
+ onSavePasswordChanged: (value) {
+ _savePasswordCB = value;
+ },
+ canSavePassword: _canSavePassword,
+ passwordSaved: widget.password != null,
+ onDomainChanged: (value) {
+ setState(() {
+ _selectedDomain = value;
+ });
+ },
+ onOriginSubmitted: () {
+ final isValid =
+ _formKey.currentState!.validate();
+ setState(() {
+ _submittButtonEnabled = isValid;
+ });
+ if (isValid) {
+ setState(() {
+ _accessDomains = _getAccessDomains();
+ });
+ }
+ },
+ onPasswordSubmitted: _submittButtonEnabled
+ ? () {
+ final isValid =
+ _formKey.currentState!.validate();
setState(() {
- _accessDomains =
- _getAccessDomains();
+ _submittButtonEnabled = isValid;
});
+ if (isValid) {
+ _onLoginButtonPressed();
+ }
}
- }
- : null,
- onPasswordSubmitted: _submittButtonEnabled
- ? () {
- final isValid =
- _formKey.currentState!.validate();
- setState(() {
- _submittButtonEnabled = isValid;
- });
- if (isValid) {
- _onLoginButtonPressed();
- }
- }
- : null,
- ),
- if (snapshot.hasData)
- Expanded(
- child: Align(
- alignment: Alignment.bottomCenter,
- child: Container(
- width: MediaQuery.of(context).size.width,
- child: FlatButton(
- onPressed: _submittButtonEnabled
- ? () {
- final isValid = _formKey
- .currentState!
- .validate();
- setState(() {
- _submittButtonEnabled =
- isValid;
- });
- if (isValid) {
- _onLoginButtonPressed();
- }
- }
- : null,
- color: ProxmoxColors.orange,
- disabledColor: Colors.grey,
- child: Text('Continue'),
- ),
- ),
- ),
+ : null,
),
- if (!snapshot.hasData)
Expanded(
child: Align(
alignment: Alignment.bottomCenter,
- child: Container(
+ child: SizedBox(
width: MediaQuery.of(context).size.width,
- child: FlatButton(
+ child: TextButton(
onPressed: _submittButtonEnabled
? () {
final isValid = _formKey
isValid;
});
if (isValid) {
- setState(() {
- _accessDomains =
- _getAccessDomains();
- });
+ if (snapshot.hasData) {
+ _onLoginButtonPressed();
+ } else {
+ setState(() {
+ _accessDomains =
+ _getAccessDomains();
+ });
+ }
}
}
: null,
- color: ProxmoxColors.orange,
- child: Text('Continue'),
- disabledColor: Colors.grey,
+ child: const Text('Continue'),
),
),
),
),
- ],
- ),
- );
- }),
+ ],
+ ),
+ );
+ }),
+ ),
),
),
),
- if (_progressModel.inProgress)
+ if (_progressModel.inProgress > 0)
ProxmoxProgressOverlay(message: _progressModel.message),
],
),
{String ticket = '', String? mRealm}) async {
setState(() {
_progressModel
- ..inProgress = true
+ ..inProgress += 1
..message = 'Authenticating...';
});
try {
final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
-
//cleaned form fields
- final origin = Uri.https(_originController.text.trim(), '');
+ final origin = normalizeUrl(_originController.text.trim());
final username = _usernameController.text.trim();
- final password =
- ticket.isNotEmpty ? ticket : _passwordController.text.trim();
+ final String enteredPassword = _passwordController.text.trim();
+ final String? savedPassword = widget.password;
+
+ final password = ticket.isNotEmpty ? ticket : enteredPassword;
final realm = _selectedDomain?.realm ?? mRealm;
var client = await proxclient.authenticate(
'$username@$realm', password, origin, settings.sslValidation!);
- if (client.credentials.tfa) {
- client = await Navigator.of(context).push(MaterialPageRoute(
+ if (client.credentials.tfa != null &&
+ client.credentials.tfa!.kinds().isNotEmpty) {
+ if (!mounted) return;
+ ProxmoxApiClient? tfaclient =
+ await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ProxmoxTfaForm(
apiClient: client,
),
));
+
+ if (tfaclient != null) {
+ client = tfaclient;
+ } else {
+ setState(() {
+ _progressModel.inProgress -= 1;
+ });
+ return;
+ }
}
final status = await client.getClusterStatus();
status.singleWhereOrNull((element) => element.local ?? false)?.name;
var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
+ final savePW = enteredPassword != '' &&
+ _savePasswordCB &&
+ enteredPassword != savedPassword;
+ final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB;
+ String? id;
+
if (widget.isCreate!) {
final newLogin = ProxmoxLoginModel((b) => b
..origin = origin
..realm = realm
..productType = ProxmoxProductType.pve
..ticket = client.credentials.ticket
+ ..passwordSaved = savePW
..hostname = hostname);
loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin));
+ id = newLogin.identifier;
} else {
loginStorage = loginStorage!.rebuild((b) => b
..logins.rebuildWhere(
(m) => m == widget.userModel,
(b) => b
..ticket = client.credentials.ticket
+ ..passwordSaved =
+ savePW || (deletePW ? false : b.passwordSaved ?? false)
..hostname = hostname));
+ id = widget.userModel!.identifier;
+ }
+
+ if (id != null) {
+ if (savePW) {
+ await savePassword(id, enteredPassword);
+ } else if (deletePW) {
+ await deletePassword(id);
+ }
}
await loginStorage.saveToDisk();
- Navigator.of(context).pop(client);
+ if (mounted) {
+ Navigator.of(context).pop(client);
+ }
} on proxclient.ProxmoxApiException catch (e) {
print(e);
+ if (!mounted) return;
if (e.message.contains('No ticket')) {
showDialog(
context: context,
builder: (context) => AlertDialog(
- title: Text('Version Error'),
- content: Text(
+ title: const Text('Version Error'),
+ content: const Text(
'Proxmox VE version not supported, please update your instance to use this app.'),
actions: [
- FlatButton(
+ TextButton(
onPressed: () => Navigator.of(context).pop(),
- child: Text('Close'),
+ child: const Text('Close'),
),
],
),
} catch (e, trace) {
print(e);
print(trace);
- if (e.runtimeType == HandshakeException) {
- showDialog(
- context: context,
- builder: (context) => ProxmoxCertificateErrorDialog(),
- );
+ if (mounted) {
+ if (e.runtimeType == HandshakeException) {
+ showDialog(
+ context: context,
+ builder: (context) => const ProxmoxCertificateErrorDialog(),
+ );
+ } else {
+ showDialog(
+ context: context,
+ builder: (context) => ConnectionErrorDialog(exception: e),
+ );
+ }
}
}
setState(() {
- _progressModel.inProgress = false;
+ _progressModel.inProgress -= 1;
});
}
- Future<List<PveAccessDomainModel?>?> _getAccessDomains() async {
- setState(() {
- _progressModel
- ..inProgress = true
- ..message = 'Connection test...';
- });
- var host = _originController.text.trim();
- var apiBaseUrl = Uri.https(host, '');
-
- RegExp portRE = new RegExp(r":\d{1,5}$");
-
- if (!portRE.hasMatch(host)) {
- _originController.text += ':8006';
- apiBaseUrl = apiBaseUrl.replace(port: 8006);
- }
-
+ Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async {
final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
List<PveAccessDomainModel?>? response;
try {
- response =
- await proxclient.accessDomains(apiBaseUrl, settings.sslValidation!);
+ response = await proxclient.accessDomains(uri, settings.sslValidation!);
} on proxclient.ProxmoxApiException catch (e) {
- showDialog(
- context: context,
- builder: (context) => ProxmoxApiErrorDialog(
- exception: e,
- ),
- );
- } catch (e, trace) {
- print(e);
- print(trace);
- if (e.runtimeType == HandshakeException) {
+ if (mounted) {
showDialog(
context: context,
- builder: (context) => ProxmoxCertificateErrorDialog(),
+ builder: (context) => ProxmoxApiErrorDialog(
+ exception: e,
+ ),
);
- } else {
+ }
+ } on HandshakeException {
+ if (mounted) {
showDialog(
context: context,
- builder: (context) => AlertDialog(
- title: Text('Connection error'),
- content: Text('Could not establish connection.'),
- actions: [
- FlatButton(
- onPressed: () => Navigator.of(context).pop(),
- child: Text('Close'),
- ),
- ],
+ builder: (context) => const ProxmoxCertificateErrorDialog(),
+ );
+ }
+ }
+ return response;
+ }
+
+ Future<List<PveAccessDomainModel?>?> _tryLoadAccessDomains(Uri uri) async {
+ List<PveAccessDomainModel?>? response;
+ try {
+ response = await _loadAccessDomains(uri);
+ } catch (e) {
+ if (mounted) {
+ showDialog(
+ context: context,
+ builder: (context) => ConnectionErrorDialog(
+ exception: e,
),
);
}
}
+ return response;
+ }
+
+ Future<List<PveAccessDomainModel?>?> _getAccessDomains() async {
+ setState(() {
+ _progressModel
+ ..inProgress += 1
+ ..message = 'Connecting...';
+ });
+
+ final canSavePW = await canSavePassword();
+ setState(() {
+ _canSavePassword = canSavePW;
+ });
+
+ var host = _originController.text.trim();
+ var apiBaseUrl = normalizeUrl(host);
+
+ RegExp portRE = RegExp(r":\d{1,5}$");
+
+ List<PveAccessDomainModel?>? response;
+
+ if (portRE.hasMatch(host)) {
+ response = await _tryLoadAccessDomains(apiBaseUrl);
+ } else {
+ // try to guess the port, 8006 first, and then 443
+ apiBaseUrl = apiBaseUrl.replace(port: 8006);
+ try {
+ response = await _loadAccessDomains(apiBaseUrl);
+ if (response != null) {
+ _originController.text = '$host:8006';
+ }
+ } catch (e) {
+ // we were no port given, and we couldn't reach on port 8006, retry with 443
+ apiBaseUrl = apiBaseUrl.replace(port: 443);
+ response = await _tryLoadAccessDomains(apiBaseUrl);
+ if (response != null) {
+ _originController.text = '$host:443';
+ }
+ }
+ }
response?.sort((a, b) => a!.realm.compareTo(b!.realm));
);
setState(() {
- _progressModel.inProgress = false;
+ _progressModel.inProgress -= 1;
_selectedDomain = selection;
});
@override
Widget build(BuildContext context) {
return Container(
- decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
+ decoration: BoxDecoration(color: Colors.black.withOpacity(0.5)),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
message,
- style: TextStyle(
- color: Theme.of(context).accentColor,
+ style: const TextStyle(
fontSize: 20,
),
),
- Padding(
- padding: const EdgeInsets.only(top: 20.0),
+ const Padding(
+ padding: EdgeInsets.only(top: 20.0),
child: CircularProgressIndicator(),
)
],
}
}
+class ConnectionErrorDialog extends StatelessWidget {
+ final Object exception;
+
+ const ConnectionErrorDialog({
+ Key? key,
+ required this.exception,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: const Text('Connection error'),
+ content: Text('Could not establish connection: $exception'),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('Close'),
+ ),
+ ],
+ );
+ }
+}
+
class ProxmoxApiErrorDialog extends StatelessWidget {
final proxclient.ProxmoxApiException exception;
@override
Widget build(BuildContext context) {
return AlertDialog(
- title: Text('API Error'),
+ title: const Text('API Error'),
content: SingleChildScrollView(
child: Text(exception.message),
),
actions: [
- FlatButton(
+ TextButton(
onPressed: () => Navigator.of(context).pop(),
- child: Text('Close'),
+ child: const Text('Close'),
),
],
);
@override
Widget build(BuildContext context) {
return AlertDialog(
- title: Text('Certificate error'),
+ title: const Text('Certificate error'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text('Your connection is not private.'),
+ const Text('Your connection is not private.'),
Text(
'Note: Consider to disable SSL validation,'
' if you use a self signed, not commonly trusted, certificate.',
- style: Theme.of(context).textTheme.caption,
+ style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
actions: [
- FlatButton(
+ TextButton(
onPressed: () => Navigator.of(context).pop(),
- child: Text('Close'),
+ child: const Text('Close'),
),
- FlatButton(
+ TextButton(
onPressed: () => Navigator.of(context).pushReplacement(
MaterialPageRoute(
- builder: (context) => ProxmoxGeneralSettingsForm())),
- child: Text('Settings'),
+ builder: (context) => const ProxmoxGeneralSettingsForm())),
+ child: const Text('Settings'),
)
],
);
}
}
+
+Uri normalizeUrl(String urlText) {
+ if (urlText.startsWith('https://')) {
+ urlText = urlText.substring('https://'.length);
+ }
+ if (urlText.startsWith('http://')) {
+ throw Exception("HTTP without TLS is not supported");
+ }
+
+ return Uri.https(urlText, '');
+}