mirror of
https://github.com/JGeek00/adguard-home-manager.git
synced 2025-04-23 23:39:12 +00:00
561 lines
No EOL
22 KiB
Dart
561 lines
No EOL
22 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:bottom_sheet/bottom_sheet.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/dhcp/select_interface_modal.dart';
|
|
|
|
import 'package:adguard_home_manager/services/http_requests.dart';
|
|
import 'package:adguard_home_manager/models/dhcp.dart';
|
|
import 'package:adguard_home_manager/providers/app_config_provider.dart';
|
|
import 'package:adguard_home_manager/providers/servers_provider.dart';
|
|
|
|
class Dhcp extends StatelessWidget {
|
|
const Dhcp({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final serversProvider = Provider.of<ServersProvider>(context);
|
|
final appConfigProvider = Provider.of<AppConfigProvider>(context);
|
|
|
|
return DhcpWidget(
|
|
serversProvider: serversProvider,
|
|
appConfigProvider: appConfigProvider
|
|
);
|
|
}
|
|
}
|
|
|
|
class DhcpWidget extends StatefulWidget {
|
|
final ServersProvider serversProvider;
|
|
final AppConfigProvider appConfigProvider;
|
|
|
|
const DhcpWidget({
|
|
Key? key,
|
|
required this.serversProvider,
|
|
required this.appConfigProvider
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<DhcpWidget> createState() => _DhcpWidgetState();
|
|
}
|
|
|
|
class _DhcpWidgetState extends State<DhcpWidget> {
|
|
DhcpModel dhcp = DhcpModel(loadStatus: 0);
|
|
|
|
NetworkInterface? selectedInterface;
|
|
|
|
final TextEditingController ipv4StartRangeController = TextEditingController();
|
|
String? ipv4StartRangeError;
|
|
final TextEditingController ipv4EndRangeController = TextEditingController();
|
|
String? ipv4EndRangeError;
|
|
final TextEditingController ipv4SubnetMaskController = TextEditingController();
|
|
String? ipv4SubnetMaskError;
|
|
final TextEditingController ipv4GatewayController = TextEditingController();
|
|
String? ipv4GatewayError;
|
|
final TextEditingController ipv4LeaseTimeController = TextEditingController();
|
|
String? ipv4LeaseTimeError;
|
|
|
|
final TextEditingController ipv6StartRangeController = TextEditingController();
|
|
String? ipv6StartRangeError;
|
|
final TextEditingController ipv6EndRangeController = TextEditingController();
|
|
String? ipv6EndRangeError;
|
|
final TextEditingController ipv6SubnetMaskController = TextEditingController();
|
|
String? ipv6SubnetMaskError;
|
|
final TextEditingController ipv6GatewayController = TextEditingController();
|
|
String? ipv6GatewayError;
|
|
final TextEditingController ipv6LeaseTimeController = TextEditingController();
|
|
String? ipv6LeaseTimeError;
|
|
|
|
void loadDhcpStatus() async {
|
|
final result = await getDhcpData(server: widget.serversProvider.selectedServer!);
|
|
|
|
if (mounted) {
|
|
if (result['result'] == 'success') {
|
|
setState(() {
|
|
dhcp.loadStatus = 1;
|
|
dhcp.data = result['data'];
|
|
});
|
|
}
|
|
else {
|
|
setState(() => dhcp.loadStatus = 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
void validateIpV4(String value, String errorVar, String errorMessage) {
|
|
void setValue(String? error) {
|
|
switch (errorVar) {
|
|
case 'ipv4StartRangeError':
|
|
setState(() => ipv4StartRangeError = error);
|
|
break;
|
|
|
|
case 'ipv4EndRangeError':
|
|
setState(() => ipv4EndRangeError = error);
|
|
break;
|
|
|
|
case 'ipv4SubnetMaskError':
|
|
setState(() => ipv4SubnetMaskError = error);
|
|
break;
|
|
|
|
case 'ipv4GatewayError':
|
|
setState(() => ipv4GatewayError = error);
|
|
break;
|
|
}
|
|
}
|
|
final regex = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$');
|
|
if (regex.hasMatch(value)) {
|
|
setValue(null);
|
|
}
|
|
else {
|
|
setValue(errorMessage);
|
|
}
|
|
}
|
|
|
|
void validateIpV6(String value, String errorVar, String errorMessage) {
|
|
void setValue(String? error) {
|
|
switch (errorVar) {
|
|
case 'ipv6StartRangeError':
|
|
setState(() => ipv4StartRangeError = error);
|
|
break;
|
|
|
|
case 'ipv6EndRangeError':
|
|
setState(() => ipv4EndRangeError = error);
|
|
break;
|
|
|
|
case 'ipv6SubnetMaskError':
|
|
setState(() => ipv4SubnetMaskError = error);
|
|
break;
|
|
|
|
case 'ipv6GatewayError':
|
|
setState(() => ipv4GatewayError = error);
|
|
break;
|
|
}
|
|
}
|
|
final regex = RegExp(r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$');
|
|
if (regex.hasMatch(value)) {
|
|
setValue(null);
|
|
}
|
|
else {
|
|
setValue(errorMessage);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
loadDhcpStatus();
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
|
|
void selectInterface() {
|
|
Future.delayed(const Duration(seconds: 0), () {
|
|
showFlexibleBottomSheet(
|
|
minHeight: 0.6,
|
|
initHeight: 0.6,
|
|
maxHeight: 0.95,
|
|
isCollapsible: true,
|
|
duration: const Duration(milliseconds: 250),
|
|
anchors: [0.95],
|
|
context: context,
|
|
builder: (ctx, controller, offset) => SelectInterfaceModal(
|
|
interfaces: dhcp.data!.networkInterfaces,
|
|
scrollController: controller,
|
|
onSelect: (interface) => setState(() => selectedInterface = interface)
|
|
),
|
|
bottomSheetColor: Colors.transparent
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget generateBody() {
|
|
switch (dhcp.loadStatus) {
|
|
case 0:
|
|
return SizedBox(
|
|
width: double.maxFinite,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const CircularProgressIndicator(),
|
|
const SizedBox(height: 30),
|
|
Text(
|
|
AppLocalizations.of(context)!.loadingDhcp,
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
color: Colors.grey,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
|
|
case 1:
|
|
if (selectedInterface != null) {
|
|
return ListView(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
top: 10,
|
|
left: 12,
|
|
right: 12
|
|
),
|
|
child: Material(
|
|
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(28),
|
|
child: InkWell(
|
|
onTap: selectedInterface != null
|
|
? () => setState(() => dhcp.data!.dhcpStatus.enabled = !dhcp.data!.dhcpStatus.enabled)
|
|
: null,
|
|
borderRadius: BorderRadius.circular(28),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 20,
|
|
vertical: 12
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
AppLocalizations.of(context)!.enableDhcpServer,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
if (selectedInterface != null) ...[
|
|
Text(
|
|
selectedInterface!.name,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey
|
|
),
|
|
)
|
|
]
|
|
],
|
|
),
|
|
Switch(
|
|
value: dhcp.data!.dhcpStatus.enabled,
|
|
onChanged: selectedInterface != null
|
|
? (value) => setState(() => dhcp.data!.dhcpStatus.enabled = value)
|
|
: null,
|
|
activeColor: Theme.of(context).primaryColor,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SectionLabel(
|
|
label: AppLocalizations.of(context)!.ipv4settings,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv4StartRangeController,
|
|
onChanged: (value) => validateIpV4(value, 'ipv4StartRangeError', AppLocalizations.of(context)!.ipNotValid),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.skip_next_rounded),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv4StartRangeError,
|
|
labelText: AppLocalizations.of(context)!.startOfRange,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv4EndRangeController,
|
|
onChanged: (value) => validateIpV4(value, 'ipv4EndRangeError', AppLocalizations.of(context)!.ipNotValid),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.skip_previous_rounded),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv4EndRangeError,
|
|
labelText: AppLocalizations.of(context)!.endOfRange,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv4SubnetMaskController,
|
|
onChanged: (value) => validateIpV4(value, 'ipv4SubnetMaskError', AppLocalizations.of(context)!.subnetMaskNotValid),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.hub_rounded),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv4SubnetMaskError,
|
|
labelText: AppLocalizations.of(context)!.subnetMask,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv4GatewayController,
|
|
onChanged: (value) => validateIpV4(value, 'ipv4GatewayError', AppLocalizations.of(context)!.gatewayNotValid),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.router_rounded),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv4GatewayError,
|
|
labelText: AppLocalizations.of(context)!.gateway,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv4LeaseTimeController,
|
|
onChanged: (value) {
|
|
if (int.tryParse(value).runtimeType == int) {
|
|
setState(() => ipv4LeaseTimeError = null);
|
|
}
|
|
else {
|
|
setState(() => ipv4LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid);
|
|
}
|
|
},
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.timer),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv4LeaseTimeError,
|
|
labelText: AppLocalizations.of(context)!.leaseTime,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
if (selectedInterface!.ipv6Addresses.isNotEmpty) ...[
|
|
SectionLabel(
|
|
label: AppLocalizations.of(context)!.ipv6settings,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv6StartRangeController,
|
|
onChanged: (value) => validateIpV4(value, 'ipv6StartRangeError', AppLocalizations.of(context)!.ipNotValid),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.skip_next_rounded),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv6StartRangeError,
|
|
labelText: AppLocalizations.of(context)!.startOfRange,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv6EndRangeController,
|
|
onChanged: (value) => validateIpV4(value, 'ipv6EndRangeError', AppLocalizations.of(context)!.ipNotValid),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.skip_previous_rounded),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv6EndRangeError,
|
|
labelText: AppLocalizations.of(context)!.endOfRange,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv6SubnetMaskController,
|
|
onChanged: (value) => validateIpV4(value, 'ipv6SubnetMaskError', AppLocalizations.of(context)!.subnetMaskNotValid),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.hub_rounded),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv6SubnetMaskError,
|
|
labelText: AppLocalizations.of(context)!.subnetMask,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv6GatewayController,
|
|
onChanged: (value) => validateIpV4(value, 'ipv6GatewayError', AppLocalizations.of(context)!.gatewayNotValid),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.router_rounded),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv6GatewayError,
|
|
labelText: AppLocalizations.of(context)!.gateway,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: TextFormField(
|
|
controller: ipv6LeaseTimeController,
|
|
onChanged: (value) {
|
|
if (int.tryParse(value).runtimeType == int) {
|
|
setState(() => ipv6LeaseTimeError = null);
|
|
}
|
|
else {
|
|
setState(() => ipv6LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid);
|
|
}
|
|
},
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.timer),
|
|
border: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(10)
|
|
)
|
|
),
|
|
errorText: ipv6LeaseTimeError,
|
|
labelText: AppLocalizations.of(context)!.leaseTime,
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
]
|
|
],
|
|
);
|
|
}
|
|
else {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Text(
|
|
AppLocalizations.of(context)!.neededSelectInterface,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
color: Colors.grey
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
ElevatedButton(
|
|
onPressed: selectInterface,
|
|
child: Text(AppLocalizations.of(context)!.selectInterface)
|
|
)
|
|
],
|
|
);
|
|
}
|
|
|
|
case 2:
|
|
return SizedBox(
|
|
width: double.maxFinite,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.error,
|
|
color: Colors.red,
|
|
size: 50,
|
|
),
|
|
const SizedBox(height: 30),
|
|
Text(
|
|
AppLocalizations.of(context)!.dhcpSettingsNotLoaded,
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
color: Colors.grey,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
|
|
default:
|
|
return const SizedBox();
|
|
}
|
|
}
|
|
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(AppLocalizations.of(context)!.dhcpSettings),
|
|
actions: selectedInterface != null ? [
|
|
IconButton(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.save_rounded),
|
|
tooltip: AppLocalizations.of(context)!.save,
|
|
),
|
|
PopupMenuButton(
|
|
itemBuilder: (context) => [
|
|
PopupMenuItem(
|
|
onTap: selectInterface,
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.swap_horiz_rounded),
|
|
const SizedBox(width: 10),
|
|
Text(AppLocalizations.of(context)!.changeInterface)
|
|
],
|
|
)
|
|
),
|
|
PopupMenuItem(
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.restore),
|
|
const SizedBox(width: 10),
|
|
Text(AppLocalizations.of(context)!.restoreConfiguration)
|
|
],
|
|
)
|
|
)
|
|
]
|
|
),
|
|
const SizedBox(width: 10)
|
|
] : null,
|
|
),
|
|
body: generateBody(),
|
|
);
|
|
}
|
|
} |