818 lines
26 KiB
Dart
818 lines
26 KiB
Dart
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/models/models.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
class LogLevelItem extends ConsumerWidget {
|
|
const LogLevelItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final logLevel = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.logLevel),
|
|
);
|
|
return ListItem<LogLevel>.options(
|
|
leading: const Icon(Icons.info_outline),
|
|
title: Text(appLocalizations.logLevel),
|
|
subtitle: Text(logLevel.name),
|
|
delegate: OptionsDelegate<LogLevel>(
|
|
title: appLocalizations.logLevel,
|
|
options: LogLevel.values,
|
|
onChanged: (LogLevel? value) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState((state) => state.copyWith(logLevel: value));
|
|
},
|
|
textBuilder: (logLevel) => logLevel.name,
|
|
value: logLevel,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class UaItem extends ConsumerWidget {
|
|
const UaItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final globalUa = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.globalUa),
|
|
);
|
|
return ListItem<String?>.options(
|
|
leading: const Icon(Icons.computer_outlined),
|
|
title: const Text('UA'),
|
|
subtitle: Text(globalUa ?? appLocalizations.defaultText),
|
|
delegate: OptionsDelegate<String?>(
|
|
title: 'UA',
|
|
options: [null, 'clash-verge/v2.4.2', 'ClashforWindows/0.19.23'],
|
|
value: globalUa,
|
|
onChanged: (value) {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState((state) => state.copyWith(globalUa: value));
|
|
},
|
|
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class KeepAliveIntervalItem extends ConsumerWidget {
|
|
const KeepAliveIntervalItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final keepAliveInterval = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.keepAliveInterval),
|
|
);
|
|
return ListItem.input(
|
|
leading: const Icon(Icons.timer_outlined),
|
|
title: Text(appLocalizations.keepAliveIntervalDesc),
|
|
subtitle: Text('$keepAliveInterval ${appLocalizations.seconds}'),
|
|
delegate: InputDelegate(
|
|
title: appLocalizations.keepAliveIntervalDesc,
|
|
suffixText: appLocalizations.seconds,
|
|
resetValue: '$defaultKeepAliveInterval',
|
|
value: '$keepAliveInterval',
|
|
validator: (String? value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(appLocalizations.interval);
|
|
}
|
|
final intValue = int.tryParse(value);
|
|
if (intValue == null) {
|
|
return appLocalizations.numberTip(appLocalizations.interval);
|
|
}
|
|
return null;
|
|
},
|
|
onChanged: (String? value) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
final intValue = int.parse(value);
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState(
|
|
(state) => state.copyWith(keepAliveInterval: intValue),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TestUrlItem extends ConsumerWidget {
|
|
const TestUrlItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final testUrl = ref.watch(
|
|
appSettingProvider.select((state) => state.testUrl),
|
|
);
|
|
return ListItem.input(
|
|
leading: const Icon(Icons.timeline),
|
|
title: Text(appLocalizations.testUrl),
|
|
subtitle: Text(testUrl),
|
|
delegate: InputDelegate(
|
|
resetValue: defaultTestUrl,
|
|
title: appLocalizations.testUrl,
|
|
value: testUrl,
|
|
validator: (String? value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(appLocalizations.testUrl);
|
|
}
|
|
if (!value.isUrl) {
|
|
return appLocalizations.urlTip(appLocalizations.testUrl);
|
|
}
|
|
return null;
|
|
},
|
|
onChanged: (String? value) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
ref
|
|
.read(appSettingProvider.notifier)
|
|
.updateState((state) => state.copyWith(testUrl: value));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PortItem extends ConsumerWidget {
|
|
const PortItem({super.key});
|
|
|
|
Future<void> handleShowPortDialog() async {
|
|
await globalState.showCommonDialog(child: _PortDialog());
|
|
// inputDelegate.onChanged(value);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final mixedPort = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.mixedPort),
|
|
);
|
|
return ListItem(
|
|
leading: const Icon(Icons.adjust_outlined),
|
|
title: Text(appLocalizations.port),
|
|
subtitle: Text('$mixedPort'),
|
|
onTap: () {
|
|
handleShowPortDialog();
|
|
},
|
|
// delegate: InputDelegate(
|
|
// title: appLocalizations.port,
|
|
// value: "$mixedPort",
|
|
// validator: (String? value) {
|
|
// if (value == null || value.isEmpty) {
|
|
// return appLocalizations.emptyTip(appLocalizations.proxyPort);
|
|
// }
|
|
// final mixedPort = int.tryParse(value);
|
|
// if (mixedPort == null) {
|
|
// return appLocalizations.numberTip(appLocalizations.proxyPort);
|
|
// }
|
|
// if (mixedPort < 1024 || mixedPort > 49151) {
|
|
// return appLocalizations.proxyPortTip;
|
|
// }
|
|
// return null;
|
|
// },
|
|
// onChanged: (String? value) {
|
|
// if (value == null) {
|
|
// return;
|
|
// }
|
|
// final mixedPort = int.parse(value);
|
|
// ref.read(patchClashConfigProvider.notifier).updateState(
|
|
// (state) => state.copyWith(
|
|
// mixedPort: mixedPort,
|
|
// ),
|
|
// );
|
|
// },
|
|
// resetValue: "$defaultMixedPort",
|
|
// ),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HostsItem extends StatelessWidget {
|
|
const HostsItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListItem.open(
|
|
leading: const Icon(Icons.view_list_outlined),
|
|
title: const Text('Hosts'),
|
|
subtitle: Text(appLocalizations.hostsDesc),
|
|
delegate: OpenDelegate(
|
|
blur: false,
|
|
title: 'Hosts',
|
|
widget: Consumer(
|
|
builder: (_, ref, _) {
|
|
final hosts = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.hosts),
|
|
);
|
|
return MapInputPage(
|
|
title: 'Hosts',
|
|
map: hosts,
|
|
titleBuilder: (item) => Text(item.key),
|
|
subtitleBuilder: (item) => Text(item.value),
|
|
onChange: (value) {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState((state) => state.copyWith(hosts: value));
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class Ipv6Item extends ConsumerWidget {
|
|
const Ipv6Item({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final ipv6 = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.ipv6),
|
|
);
|
|
return ListItem.switchItem(
|
|
leading: const Icon(Icons.water_outlined),
|
|
title: const Text('IPv6'),
|
|
subtitle: Text(appLocalizations.ipv6Desc),
|
|
delegate: SwitchDelegate(
|
|
value: ipv6,
|
|
onChanged: (bool value) async {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState((state) => state.copyWith(ipv6: value));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AppendSystemDNSItem extends ConsumerWidget {
|
|
const AppendSystemDNSItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final appendSystemDNS = ref.watch(
|
|
networkSettingProvider.select((state) => state.appendSystemDns),
|
|
);
|
|
return ListItem.switchItem(
|
|
leading: const Icon(Icons.dns_outlined),
|
|
title: Text(appLocalizations.appendSystemDns),
|
|
subtitle: Text(appLocalizations.appendSystemDnsTip),
|
|
delegate: SwitchDelegate(
|
|
value: appendSystemDNS,
|
|
onChanged: (bool value) async {
|
|
ref
|
|
.read(networkSettingProvider.notifier)
|
|
.updateState((state) => state.copyWith(appendSystemDns: value));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AllowLanItem extends ConsumerWidget {
|
|
const AllowLanItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final allowLan = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.allowLan),
|
|
);
|
|
return ListItem.switchItem(
|
|
leading: const Icon(Icons.device_hub),
|
|
title: Text(appLocalizations.allowLan),
|
|
subtitle: Text(appLocalizations.allowLanDesc),
|
|
delegate: SwitchDelegate(
|
|
value: allowLan,
|
|
onChanged: (bool value) async {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState((state) => state.copyWith(allowLan: value));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class UnifiedDelayItem extends ConsumerWidget {
|
|
const UnifiedDelayItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final unifiedDelay = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.unifiedDelay),
|
|
);
|
|
|
|
return ListItem.switchItem(
|
|
leading: const Icon(Icons.compress_outlined),
|
|
title: Text(appLocalizations.unifiedDelay),
|
|
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
|
delegate: SwitchDelegate(
|
|
value: unifiedDelay,
|
|
onChanged: (bool value) async {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState((state) => state.copyWith(unifiedDelay: value));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class FindProcessItem extends ConsumerWidget {
|
|
const FindProcessItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final findProcess = ref.watch(
|
|
patchClashConfigProvider.select(
|
|
(state) => state.findProcessMode == FindProcessMode.always,
|
|
),
|
|
);
|
|
|
|
return ListItem.switchItem(
|
|
leading: const Icon(Icons.polymer_outlined),
|
|
title: Text(appLocalizations.findProcessMode),
|
|
subtitle: Text(appLocalizations.findProcessModeDesc),
|
|
delegate: SwitchDelegate(
|
|
value: findProcess,
|
|
onChanged: (bool value) async {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState(
|
|
(state) => state.copyWith(
|
|
findProcessMode: value
|
|
? FindProcessMode.always
|
|
: FindProcessMode.off,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TcpConcurrentItem extends ConsumerWidget {
|
|
const TcpConcurrentItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final tcpConcurrent = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.tcpConcurrent),
|
|
);
|
|
return ListItem.switchItem(
|
|
leading: const Icon(Icons.double_arrow_outlined),
|
|
title: Text(appLocalizations.tcpConcurrent),
|
|
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
|
delegate: SwitchDelegate(
|
|
value: tcpConcurrent,
|
|
onChanged: (value) async {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState((state) => state.copyWith(tcpConcurrent: value));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class GeodataLoaderItem extends ConsumerWidget {
|
|
const GeodataLoaderItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final isMemconservative = ref.watch(
|
|
patchClashConfigProvider.select(
|
|
(state) => state.geodataLoader == GeodataLoader.memconservative,
|
|
),
|
|
);
|
|
return ListItem.switchItem(
|
|
leading: const Icon(Icons.memory),
|
|
title: Text(appLocalizations.geodataLoader),
|
|
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
|
delegate: SwitchDelegate(
|
|
value: isMemconservative,
|
|
onChanged: (bool value) async {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState(
|
|
(state) => state.copyWith(
|
|
geodataLoader: value
|
|
? GeodataLoader.memconservative
|
|
: GeodataLoader.standard,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ExternalControllerItem extends ConsumerWidget {
|
|
const ExternalControllerItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final hasExternalController = ref.watch(
|
|
patchClashConfigProvider.select(
|
|
(state) => state.externalController == ExternalControllerStatus.open,
|
|
),
|
|
);
|
|
return ListItem.switchItem(
|
|
leading: const Icon(Icons.api_outlined),
|
|
title: Text(appLocalizations.externalController),
|
|
subtitle: Text(appLocalizations.externalControllerDesc),
|
|
delegate: SwitchDelegate(
|
|
value: hasExternalController,
|
|
onChanged: (bool value) async {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState(
|
|
(state) => state.copyWith(
|
|
externalController: value
|
|
? ExternalControllerStatus.open
|
|
: ExternalControllerStatus.close,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
final generalItems = <Widget>[
|
|
LogLevelItem(),
|
|
UaItem(),
|
|
if (system.isDesktop) KeepAliveIntervalItem(),
|
|
TestUrlItem(),
|
|
PortItem(),
|
|
HostsItem(),
|
|
Ipv6Item(),
|
|
AllowLanItem(),
|
|
UnifiedDelayItem(),
|
|
AppendSystemDNSItem(),
|
|
FindProcessItem(),
|
|
TcpConcurrentItem(),
|
|
GeodataLoaderItem(),
|
|
ExternalControllerItem(),
|
|
].separated(const Divider(height: 0)).toList();
|
|
|
|
class _PortDialog extends ConsumerStatefulWidget {
|
|
const _PortDialog();
|
|
|
|
@override
|
|
ConsumerState<_PortDialog> createState() => _PortDialogState();
|
|
}
|
|
|
|
class _PortDialogState extends ConsumerState<_PortDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
bool _isMore = false;
|
|
|
|
late TextEditingController _mixedPortController;
|
|
late TextEditingController _portController;
|
|
late TextEditingController _socksPortController;
|
|
late TextEditingController _redirPortController;
|
|
late TextEditingController _tProxyPortController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final vm5 = ref.read(
|
|
patchClashConfigProvider.select((state) {
|
|
return VM5(
|
|
a: state.mixedPort,
|
|
b: state.port,
|
|
c: state.socksPort,
|
|
d: state.redirPort,
|
|
e: state.tproxyPort,
|
|
);
|
|
}),
|
|
);
|
|
_mixedPortController = TextEditingController(text: vm5.a.toString());
|
|
_portController = TextEditingController(text: vm5.b.toString());
|
|
_socksPortController = TextEditingController(text: vm5.c.toString());
|
|
_redirPortController = TextEditingController(text: vm5.d.toString());
|
|
_tProxyPortController = TextEditingController(text: vm5.e.toString());
|
|
}
|
|
|
|
Future<void> _handleReset() async {
|
|
final res = await globalState.showMessage(
|
|
message: TextSpan(text: appLocalizations.resetTip),
|
|
);
|
|
if (res != true) {
|
|
return;
|
|
}
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState(
|
|
(state) => state.copyWith(
|
|
mixedPort: 7890,
|
|
port: 0,
|
|
socksPort: 0,
|
|
redirPort: 0,
|
|
tproxyPort: 0,
|
|
),
|
|
);
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
|
|
void _handleUpdate() {
|
|
if (_formKey.currentState?.validate() == false) return;
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.updateState(
|
|
(state) => state.copyWith(
|
|
mixedPort: int.parse(_mixedPortController.text),
|
|
port: int.parse(_portController.text),
|
|
socksPort: int.parse(_socksPortController.text),
|
|
redirPort: int.parse(_redirPortController.text),
|
|
tproxyPort: int.parse(_tProxyPortController.text),
|
|
),
|
|
);
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
void _handleMore() {
|
|
setState(() {
|
|
_isMore = !_isMore;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CommonDialog(
|
|
title: appLocalizations.port,
|
|
actions: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
IconButton.filledTonal(
|
|
onPressed: _handleMore,
|
|
icon: CommonExpandIcon(expand: _isMore),
|
|
),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: _handleReset,
|
|
child: Text(appLocalizations.reset),
|
|
),
|
|
const SizedBox(width: 4),
|
|
TextButton(
|
|
onPressed: _handleUpdate,
|
|
child: Text(appLocalizations.submit),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
child: Form(
|
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
key: _formKey,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(top: 8),
|
|
child: AnimatedSize(
|
|
duration: midDuration,
|
|
curve: Curves.easeOutQuad,
|
|
alignment: Alignment.topCenter,
|
|
child: Column(
|
|
spacing: 24,
|
|
children: [
|
|
TextFormField(
|
|
keyboardType: TextInputType.url,
|
|
maxLines: 1,
|
|
minLines: 1,
|
|
controller: _mixedPortController,
|
|
onFieldSubmitted: (_) {
|
|
_handleUpdate();
|
|
},
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.mixedPort,
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(
|
|
appLocalizations.mixedPort,
|
|
);
|
|
}
|
|
final port = int.tryParse(value);
|
|
if (port == null) {
|
|
return appLocalizations.numberTip(
|
|
appLocalizations.mixedPort,
|
|
);
|
|
}
|
|
if (port < 1024 || port > 49151) {
|
|
return appLocalizations.portTip(
|
|
appLocalizations.mixedPort,
|
|
);
|
|
}
|
|
final ports = [
|
|
_portController.text,
|
|
_socksPortController.text,
|
|
_tProxyPortController.text,
|
|
_redirPortController.text,
|
|
].map((item) => item.trim());
|
|
if (ports.contains(value.trim())) {
|
|
return appLocalizations.portConflictTip;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
if (_isMore) ...[
|
|
TextFormField(
|
|
keyboardType: TextInputType.url,
|
|
maxLines: 1,
|
|
minLines: 1,
|
|
controller: _portController,
|
|
onFieldSubmitted: (_) {
|
|
_handleUpdate();
|
|
},
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.port,
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(appLocalizations.port);
|
|
}
|
|
final port = int.tryParse(value);
|
|
if (port == null) {
|
|
return appLocalizations.numberTip(
|
|
appLocalizations.port,
|
|
);
|
|
}
|
|
if (port == 0) {
|
|
return null;
|
|
}
|
|
if (port < 1024 || port > 49151) {
|
|
return appLocalizations.portTip(appLocalizations.port);
|
|
}
|
|
final ports = [
|
|
_mixedPortController.text,
|
|
_socksPortController.text,
|
|
_tProxyPortController.text,
|
|
_redirPortController.text,
|
|
].map((item) => item.trim());
|
|
if (ports.contains(value.trim())) {
|
|
return appLocalizations.portConflictTip;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
TextFormField(
|
|
keyboardType: TextInputType.url,
|
|
maxLines: 1,
|
|
minLines: 1,
|
|
controller: _socksPortController,
|
|
onFieldSubmitted: (_) {
|
|
_handleUpdate();
|
|
},
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.socksPort,
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(
|
|
appLocalizations.socksPort,
|
|
);
|
|
}
|
|
final port = int.tryParse(value);
|
|
if (port == null) {
|
|
return appLocalizations.numberTip(
|
|
appLocalizations.socksPort,
|
|
);
|
|
}
|
|
if (port == 0) {
|
|
return null;
|
|
}
|
|
if (port < 1024 || port > 49151) {
|
|
return appLocalizations.portTip(
|
|
appLocalizations.socksPort,
|
|
);
|
|
}
|
|
final ports = [
|
|
_portController.text,
|
|
_mixedPortController.text,
|
|
_tProxyPortController.text,
|
|
_redirPortController.text,
|
|
].map((item) => item.trim());
|
|
if (ports.contains(value.trim())) {
|
|
return appLocalizations.portConflictTip;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
TextFormField(
|
|
keyboardType: TextInputType.url,
|
|
maxLines: 1,
|
|
minLines: 1,
|
|
controller: _redirPortController,
|
|
onFieldSubmitted: (_) {
|
|
_handleUpdate();
|
|
},
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.redirPort,
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(
|
|
appLocalizations.redirPort,
|
|
);
|
|
}
|
|
final port = int.tryParse(value);
|
|
if (port == null) {
|
|
return appLocalizations.numberTip(
|
|
appLocalizations.redirPort,
|
|
);
|
|
}
|
|
if (port == 0) {
|
|
return null;
|
|
}
|
|
if (port < 1024 || port > 49151) {
|
|
return appLocalizations.portTip(
|
|
appLocalizations.redirPort,
|
|
);
|
|
}
|
|
final ports = [
|
|
_portController.text,
|
|
_socksPortController.text,
|
|
_tProxyPortController.text,
|
|
_mixedPortController.text,
|
|
].map((item) => item.trim());
|
|
if (ports.contains(value.trim())) {
|
|
return appLocalizations.portConflictTip;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
TextFormField(
|
|
keyboardType: TextInputType.url,
|
|
maxLines: 1,
|
|
minLines: 1,
|
|
controller: _tProxyPortController,
|
|
onFieldSubmitted: (_) {
|
|
_handleUpdate();
|
|
},
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.tproxyPort,
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.emptyTip(
|
|
appLocalizations.tproxyPort,
|
|
);
|
|
}
|
|
final port = int.tryParse(value);
|
|
if (port == null) {
|
|
return appLocalizations.numberTip(
|
|
appLocalizations.tproxyPort,
|
|
);
|
|
}
|
|
if (port == 0) {
|
|
return null;
|
|
}
|
|
if (port < 1024 || port > 49151) {
|
|
return appLocalizations.portTip(
|
|
appLocalizations.tproxyPort,
|
|
);
|
|
}
|
|
final ports = [
|
|
_portController.text,
|
|
_socksPortController.text,
|
|
_mixedPortController.text,
|
|
_redirPortController.text,
|
|
].map((item) => item.trim());
|
|
if (ports.contains(value.trim())) {
|
|
return appLocalizations.portConflictTip;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|