Files
MWClash/lib/views/config/general.dart
chen08209 d3c3f04062 Fix android tile service
Support append system DNS

Fix some issues
2025-10-08 16:28:02 +08:00

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