Files
MWClash/lib/views/config/general.dart
chen08209 afbc5adb05 Support override script
Support proxies search

Support svg display

Optimize config persistence

Add some scenes auto close connections

Update core

Optimize more details
2025-06-07 23:52:27 +08:00

794 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/v1.6.6",
"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});
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(),
);
}
_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();
}
}
_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();
}
_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;
},
),
]
],
),
),
),
),
);
}
}