Support core status check and force restart Optimize proxies page and access page Update flutter and pub dependencies Update go version Optimize more details
793 lines
26 KiB
Dart
793 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 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(),
|
|
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;
|
|
},
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|