Added check and save encryption settings

This commit is contained in:
Juan Gilsanz Polo 2022-10-23 04:50:12 +02:00
parent 137a976a36
commit 3a2483e9d6
7 changed files with 535 additions and 31 deletions

View file

@ -515,5 +515,13 @@
"privateKeyFile": "Set a private key file",
"pastePrivateKey": "Paste the private key contents",
"usePreviousKey": "Use the previously saved key",
"privateKeyPath": "Private key path"
"privateKeyPath": "Private key path",
"invalidCertificate": "Invalid certificate",
"invalidPrivateKey": "Invalid private key",
"validatingData": "Validating data",
"dataValid": "Data is valid",
"dataNotValid": "Data not valid",
"encryptionConfigSaved": "Encryption configuration saved successfully",
"encryptionConfigNotSaved": "Encryption configuration could not be saved",
"configError": "Configuration error"
}

View file

@ -515,5 +515,13 @@
"privateKeyFile": "Establecer un archivo de clave privada",
"pastePrivateKey": "Pegar el contenido de la clave privada",
"usePreviousKey": "Usar la clave privada guardada previamente",
"privateKeyPath": "Ruta de la clave privada"
"privateKeyPath": "Ruta de la clave privada",
"invalidCertificate": "Certificado no válido",
"invalidPrivateKey": "Clave privada no válida",
"validatingData": "Validando datos",
"dataValid": "Datos válidos",
"dataNotValid": "Datos no válidos",
"encryptionConfigSaved": "Configuración de cifrado guardada correctamente",
"encryptionConfigNotSaved": "No se pudo guardar la configuración de encriptado",
"configError": "Configuration error"
}

View file

@ -17,16 +17,20 @@ String encryptionDataToJson(EncryptionData data) => json.encode(data.toJson());
class EncryptionData {
final bool validCert;
final bool validChain;
final String? subject;
final String? issuer;
final DateTime notBefore;
final DateTime notAfter;
final dynamic dnsNames;
final List<String> dnsNames;
final bool validKey;
final String? keyType;
final bool validPair;
final bool enabled;
final String? serverName;
final bool forceHttps;
final int portHttps;
final int portDnsOverTls;
final int portDnsOverQuic;
final int? portHttps;
final int? portDnsOverTls;
final int? portDnsOverQuic;
final int portDnscrypt;
final String dnscryptConfigFile;
final bool allowUnencryptedDoh;
@ -39,16 +43,20 @@ class EncryptionData {
EncryptionData({
required this.validCert,
required this.validChain,
this.subject,
this.issuer,
required this.notBefore,
required this.notAfter,
required this.dnsNames,
required this.validKey,
this.keyType,
required this.validPair,
required this.enabled,
this.serverName,
required this.forceHttps,
required this.portHttps,
required this.portDnsOverTls,
required this.portDnsOverQuic,
this.portHttps,
this.portDnsOverTls,
this.portDnsOverQuic,
required this.portDnscrypt,
required this.dnscryptConfigFile,
required this.allowUnencryptedDoh,
@ -63,12 +71,16 @@ class EncryptionData {
factory EncryptionData.fromJson(Map<String, dynamic> json) => EncryptionData(
validCert: json["valid_cert"],
validChain: json["valid_chain"],
subject: json["subject"],
issuer: json["issuer"],
notBefore: DateTime.parse(json["not_before"]),
notAfter: DateTime.parse(json["not_after"]),
dnsNames: json["dns_names"],
dnsNames: json["dns_names"] != null ? List<String>.from(json["dns_names"].map((x) => x)) : [],
validKey: json["valid_key"],
keyType: json["key_type"],
validPair: json["valid_pair"],
enabled: json["enabled"],
serverName: json["server_name"],
forceHttps: json["force_https"],
portHttps: json["port_https"],
portDnsOverTls: json["port_dns_over_tls"],
@ -86,12 +98,16 @@ class EncryptionData {
Map<String, dynamic> toJson() => {
"valid_cert": validCert,
"valid_chain": validChain,
"subject": subject,
"issuer": issuer,
"not_before": notBefore.toIso8601String(),
"not_after": notAfter.toIso8601String(),
"dns_names": dnsNames,
"dns_names": List<dynamic>.from(dnsNames.map((x) => x)),
"valid_key": validKey,
"key_type": keyType,
"valid_pair": validPair,
"enabled": enabled,
"server_name": serverName,
"force_https": forceHttps,
"port_https": portHttps,
"port_dns_over_tls": portDnsOverTls,

View file

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class EncryptionErrorModal extends StatelessWidget {
final String error;
const EncryptionErrorModal({
Key? key,
required this.error,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.configError),
content: Text(error),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.close)
)
],
);
}
}

View file

@ -1,10 +1,15 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/section_label.dart';
import 'package:adguard_home_manager/screens/settings/encryption/config_error_modal.dart';
import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
@ -73,6 +78,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
final TextEditingController pastePrivateKeyController = TextEditingController();
String? pastePrivateKeyError;
bool validData = false;
String? validDataError;
int dataValidApi = 0;
void fetchData({bool? showRefreshIndicator}) async {
widget.serversProvider.setEncryptionSettingsLoadStatus(0, showRefreshIndicator ?? false);
@ -80,7 +88,35 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
final result = await getEncryptionSettings(server: widget.serversProvider.selectedServer!);
if (mounted) {
await checkValidDataApi();
if (result['result'] == 'success') {
setState(() {
enabled = result['data'].enabled;
domainNameController.text = result['data'].serverName ?? '';
redirectHttps = result['data'].forceHttps;
httpsPortController.text = result['data'].portHttps != null ? result['data'].portHttps.toString() : '';
tlsPortController.text = result['data'].portDnsOverTls != null ? result['data'].portDnsOverTls.toString() : '';
dnsOverQuicPortController.text = result['data'].portDnsOverQuic != null ? result['data'].portDnsOverQuic.toString() : '';
if (result['data'].certificateChain != '' && result['data'].certificatePath == '') {
certificateOption = 1;
certificateContentController.text = result['data'].certificateChain;
}
else if (result['data'].certificateChain == '' && result['data'].certificatePath != '') {
certificateOption = 0;
certificatePathController.text = result['data'].certificatePath;
}
if (result['data'].privateKey != '' && result['data'].privateKeyPath == '') {
privateKeyOption = 1;
pastePrivateKeyController.text = result['data'].privateKey;
}
else if (result['data'].privateKey == '' && result['data'].privateKeyPath != '') {
privateKeyOption = 0;
privateKeyPathController.text = result['data'].privateKeyPath;
}
usePreviouslySavedKey = result['data'].privateKeySaved;
});
widget.serversProvider.setEncryptionSettings(result['data']);
widget.serversProvider.setEncryptionSettingsLoadStatus(1, true);
}
@ -91,9 +127,186 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
}
}
void validateDomain(String domain) {
RegExp regExp = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$');
if (regExp.hasMatch(domain)) {
setState(() => domainError = null);
checkValidDataApi();
}
else {
setState(() => domainError = AppLocalizations.of(context)!.domainNotValid);
}
checkDataValid();
}
void validatePort(String value, String portType) {
if (int.tryParse(value) != null && int.parse(value) <= 65535) {
setState(() {
switch (portType) {
case 'https':
setState(() => httpsPortError = null);
break;
case 'tls':
setState(() => tlsPortError = null);
break;
case 'quic':
setState(() => dnsOverQuicPortError = null);
break;
default:
break;
}
});
checkValidDataApi();
}
else {
setState(() {
switch (portType) {
case 'https':
setState(() => httpsPortError = AppLocalizations.of(context)!.invalidPort);
break;
case 'tls':
setState(() => tlsPortError = AppLocalizations.of(context)!.invalidPort);
break;
case 'quic':
setState(() => dnsOverQuicPortError = AppLocalizations.of(context)!.invalidPort);
break;
default:
break;
}
});
}
checkDataValid();
}
void validateCertificate(String cert) {
final regExp = RegExp(r'(-{3,}(\bBEGIN CERTIFICATE\b))|(-{3,}-{3,}(\END CERTIFICATE\b)-{3,})', multiLine: true);
if (regExp.hasMatch(cert.replaceAll('\n', ''))) {
setState(() => certificateContentError = AppLocalizations.of(context)!.invalidCertificate);
}
else {
setState(() => certificateContentError = null);
}
checkDataValid();
checkValidDataApi();
}
void validatePrivateKey(String cert) {
final regExp = RegExp(r'(-{3,}(\bBEGIN\b).*(PRIVATE KEY\b))|(-{3,}-{3,}(\bEND\b).*(PRIVATE KEY\b)-{3,})', multiLine: true);
if (regExp.hasMatch(cert.replaceAll('\n', ''))) {
setState(() => pastePrivateKeyError = AppLocalizations.of(context)!.invalidPrivateKey);
}
else {
setState(() => pastePrivateKeyError = null);
}
checkValidDataApi();
}
void validatePath(String cert, String item) {
final regExp = RegExp(r'^(\/{0,1}(?!\/))[A-Za-z0-9\/\-_]+(\.([a-zA-Z]+))?$');
if (regExp.hasMatch(cert)) {
if (item == 'cert') {
setState(() => certificatePathError = null);
}
else if (item == 'private_key') {
setState(() => privateKeyPathError = null);
}
checkValidDataApi();
}
else {
if (item == 'cert') {
setState(() => certificatePathError = AppLocalizations.of(context)!.invalidPath);
}
else if (item == 'private_key') {
setState(() => privateKeyPathError = AppLocalizations.of(context)!.invalidPath);
}
}
checkDataValid();
}
Future checkValidDataApi() async {
setState(() => dataValidApi = 0);
final result = await checkEncryptionSettings(server: widget.serversProvider.selectedServer!, data: {
"enabled": enabled,
"server_name": domainNameController.text,
"force_https": redirectHttps,
"port_https": httpsPortController.text != '' ? int.parse(httpsPortController.text) : null,
"port_dns_over_tls": tlsPortController.text != '' ? int.parse(tlsPortController.text) : null,
"port_dns_over_quic": dnsOverQuicPortController.text != '' ? int.parse(dnsOverQuicPortController.text) : null,
"certificate_chain": certificateContentController.text.replaceAll('\n', ''),
"private_key": pastePrivateKeyController.text.replaceAll('\n', ''),
"private_key_saved": usePreviouslySavedKey,
"certificate_path": certificatePathController.text,
"private_key_path": privateKeyPathController.text,
});
if (result['result'] == 'success') {
setState(() {
if (result['data']['warning_validation'] != null && result['data']['warning_validation'] != '') {
dataValidApi = 2;
validDataError = result['data']['warning_validation'];
}
else {
dataValidApi = 1;
validDataError = null;
}
});
}
else {
if (result['log'].resBody != null) {
setState(() => validDataError = result['log'].resBody);
}
setState(() => dataValidApi = 2);
}
}
void checkDataValid() {
if (
domainNameController.text != '' &&
domainError == null &&
httpsPortController.text != '' &&
httpsPortError == null &&
tlsPortController.text != '' &&
tlsPortError == null &&
dnsOverQuicPortController.text != '' &&
dnsOverQuicPortError == null &&
((
certificateOption == 0 &&
certificatePathController.text != '' &&
certificatePathError == null
) || (
certificateOption == 1 &&
certificateContentController.text != '' &&
certificateContentError == null
)) &&
((
privateKeyOption == 0 &&
privateKeyPathController.text != '' &&
privateKeyPathError == null
) || (
privateKeyOption == 1 &&
pastePrivateKeyController.text != '' &&
pastePrivateKeyError == null
))
) {
setState(() => validData = true);
checkValidDataApi();
}
else {
setState(() => validData = false);
}
}
@override
void initState() {
fetchData();
super.initState();
}
@ -102,6 +315,88 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
void saveData() async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.savingConfig);
final result = await saveEncryptionSettings(server: serversProvider.selectedServer!, data: {
"enabled": enabled,
"server_name": domainNameController.text,
"force_https": redirectHttps,
"port_https": int.parse(httpsPortController.text),
"port_dns_over_tls": int.parse(tlsPortController.text),
"port_dns_over_quic": int.parse(dnsOverQuicPortController.text),
"certificate_chain": certificateContentController.text,
"private_key": pastePrivateKeyController.text,
"private_key_saved": usePreviouslySavedKey,
"certificate_path": certificatePathController.text,
"private_key_path": privateKeyPathController.text,
});
processModal.close();
if (result['result'] == 'success') {
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.encryptionConfigSaved,
color: Colors.green
);
}
else {
appConfigProvider.addLog(result['log']);
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.encryptionConfigNotSaved,
color: Colors.red
);
}
}
Widget generateStatus() {
if (dataValidApi == 0) {
return const SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator(
strokeWidth: 3,
)
);
}
else if (dataValidApi == 1) {
return const Icon(
Icons.check_circle_rounded,
color: Colors.green,
);
}
else if (dataValidApi == 2) {
return const Icon(
Icons.cancel_rounded,
color: Colors.red,
);
}
else {
return const SizedBox();
}
}
String generateStatusString() {
if (dataValidApi == 0) {
return AppLocalizations.of(context)!.validatingData;
}
else if (dataValidApi == 1) {
return AppLocalizations.of(context)!.dataValid;
}
else if (dataValidApi == 2) {
return AppLocalizations.of(context)!.dataNotValid;
}
else {
return "";
}
}
Widget generateBody() {
switch (widget.serversProvider.encryptionSettings.loadStatus) {
case 0:
@ -138,7 +433,11 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: () => setState(() => enabled = !enabled),
onTap: () {
setState(() => enabled = !enabled);
checkDataValid();
checkValidDataApi();
},
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
@ -171,7 +470,11 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
),
Switch(
value: enabled,
onChanged: (value) => setState(() => enabled = value),
onChanged: (value) {
setState(() => enabled = value);
checkDataValid();
checkValidDataApi();
},
activeColor: Theme.of(context).primaryColor,
),
],
@ -184,8 +487,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: enabled,
controller: domainNameController,
// onChanged:
onChanged: validateDomain,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link_rounded),
border: const OutlineInputBorder(
@ -196,6 +500,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
errorText: domainError,
labelText: AppLocalizations.of(context)!.domainName,
helperText: AppLocalizations.of(context)!.domainNameDescription,
helperStyle: TextStyle(
color: Theme.of(context).listTileTheme.iconColor
),
helperMaxLines: 10
),
),
@ -203,15 +510,21 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
const SizedBox(height: 10),
CustomSwitchListTile(
value: redirectHttps,
onChanged: (value) => setState(() => redirectHttps = value),
onChanged: (value) {
setState(() => redirectHttps = value);
checkDataValid();
checkValidDataApi();
},
title: AppLocalizations.of(context)!.redirectHttps,
disabled: !enabled,
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: enabled,
controller: httpsPortController,
// onChanged:
onChanged: (value) => validatePort(value, 'https'),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.numbers_rounded),
border: const OutlineInputBorder(
@ -229,8 +542,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: enabled,
controller: tlsPortController,
// onChanged:
onChanged: (value) => validatePort(value, 'tls'),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.numbers_rounded),
border: const OutlineInputBorder(
@ -248,8 +562,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: enabled,
controller: dnsOverQuicPortController,
// onChanged:
onChanged: (value) => validatePort(value, 'quic'),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.numbers_rounded),
border: const OutlineInputBorder(
@ -283,7 +598,13 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
RadioListTile(
value: 0,
groupValue: certificateOption,
onChanged: (value) => setState(() => certificateOption = value!),
onChanged: enabled == true
? (value) {
setState(() => certificateOption = int.parse(value.toString()));
checkDataValid();
checkValidDataApi();
}
: null,
title: Text(
AppLocalizations.of(context)!.certificateFilePath,
style: const TextStyle(
@ -294,7 +615,13 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
RadioListTile(
value: 1,
groupValue: certificateOption,
onChanged: (value) => setState(() => certificateOption = value!),
onChanged: enabled == true
? (value) {
setState(() => certificateOption = int.parse(value.toString()));
checkDataValid();
checkValidDataApi();
}
: null,
title: Text(
AppLocalizations.of(context)!.pasteCertificateContent,
style: const TextStyle(
@ -306,8 +633,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
if (certificateOption == 0) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: enabled,
controller: certificatePathController,
// onChanged:
onChanged: (value) => validatePath(value, 'cert'),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.description_rounded),
border: const OutlineInputBorder(
@ -323,8 +651,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
if (certificateOption == 1) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: enabled,
controller: certificateContentController,
// onChanged:
onChanged: validateCertificate,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.description_rounded),
border: const OutlineInputBorder(
@ -343,7 +672,13 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
RadioListTile(
value: 0,
groupValue: privateKeyOption,
onChanged: (value) => setState(() => privateKeyOption = value!),
onChanged: enabled == true
? (value) {
setState(() => privateKeyOption = int.parse(value.toString()));
checkDataValid();
checkValidDataApi();
}
: null,
title: Text(
AppLocalizations.of(context)!.privateKeyFile,
style: const TextStyle(
@ -354,7 +689,13 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
RadioListTile(
value: 1,
groupValue: privateKeyOption,
onChanged: (value) => setState(() => privateKeyOption = value!),
onChanged: enabled == true
? (value) {
setState(() => privateKeyOption = int.parse(value.toString()));
checkDataValid();
checkValidDataApi();
}
: null,
title: Text(
AppLocalizations.of(context)!.pastePrivateKey,
style: const TextStyle(
@ -374,8 +715,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
if (privateKeyOption == 0) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: enabled,
controller: privateKeyPathController,
// onChanged:
onChanged: (value) => validatePath(value, 'private_key'),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.description_rounded),
border: const OutlineInputBorder(
@ -391,9 +733,11 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
if (privateKeyOption == 1) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: !usePreviouslySavedKey,
enabled: enabled == true
? !usePreviouslySavedKey
: false,
controller: pastePrivateKeyController,
// onChanged:
onChanged: validatePrivateKey,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.description_rounded),
border: const OutlineInputBorder(
@ -445,6 +789,27 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.encryptionSettings),
actions: [
IconButton(
onPressed: validData == true && dataValidApi == 2 && validDataError != null
? () => {
showDialog(
context: context,
builder: (context) => EncryptionErrorModal(error: validDataError!)
)
} : null,
icon: generateStatus(),
tooltip: generateStatusString()
),
IconButton(
onPressed: dataValidApi == 1
? () => saveData()
: null,
icon: const Icon(Icons.save),
tooltip: AppLocalizations.of(context)!.save,
),
const SizedBox(width: 10),
],
),
body: generateBody(),
);

View file

@ -1740,4 +1740,75 @@ Future getEncryptionSettings({
else {
return result;
}
}
Future checkEncryptionSettings({
required Server server,
required Map<String, dynamic> data,
}) async {
final result = await apiRequest(
urlPath: '/tls/validate',
method: 'post',
server: server,
body: data,
type: 'check_encryption_settings'
);
if (result['hasResponse'] == true) {
if (result['statusCode'] == 200) {
return {
'result': 'success',
'data': jsonDecode(result['body'])
};
}
else {
return {
'result': 'error',
'log': AppLog(
type: 'check_encryption_settings',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
statusCode: result['statusCode'].toString(),
resBody: result['body'],
)
};
}
}
else {
return result;
}
}
Future saveEncryptionSettings({
required Server server,
required Map<String, dynamic> data,
}) async {
final result = await apiRequest(
urlPath: '/tls/configure',
method: 'post',
server: server,
body: data,
type: 'update_encryption_settings'
);
if (result['hasResponse'] == true) {
if (result['statusCode'] == 200) {
return { 'result': 'success' };
}
else {
return {
'result': 'error',
'log': AppLog(
type: 'update_encryption_settings',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
statusCode: result['statusCode'].toString(),
resBody: result['body'],
)
};
}
}
else {
return result;
}
}

View file

@ -5,12 +5,14 @@ class CustomSwitchListTile extends StatelessWidget {
final void Function(bool) onChanged;
final String title;
final String? subtitle;
final bool? disabled;
const CustomSwitchListTile({
Key? key,
required this.value,
required this.onChanged,
required this.title,
this.disabled,
this.subtitle,
}) : super(key: key);
@ -19,7 +21,9 @@ class CustomSwitchListTile extends StatelessWidget {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => onChanged(!value),
onTap: disabled != null && disabled == true
? null
: () => onChanged(!value),
child: Padding(
padding: const EdgeInsets.only(
top: 20, left: 24, right: 10, bottom: 20
@ -34,8 +38,11 @@ class CustomSwitchListTile extends StatelessWidget {
width: MediaQuery.of(context).size.width-110,
child: Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
color: disabled != null && disabled == true
? Colors.grey
: null,
),
),
),
@ -47,7 +54,9 @@ class CustomSwitchListTile extends StatelessWidget {
subtitle!,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).listTileTheme.iconColor,
color: disabled != null && disabled == true
? Colors.grey
: Theme.of(context).listTileTheme.iconColor,
),
),
),
@ -56,7 +65,9 @@ class CustomSwitchListTile extends StatelessWidget {
),
Switch(
value: value,
onChanged: onChanged,
onChanged: disabled != null && disabled == true
? null
: onChanged,
activeColor: Theme.of(context).primaryColor,
)
],