Files
MWClash/lib/views/config/general.dart
chen08209 ed7868282a Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies

Update go version

Optimize more details
2025-09-23 15:23:58 +08:00

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;
},
),
],
],
),
),
),
),
);
}
}