d780cb863d
Changelog: The elevated Core only answers its owner
523 lines
16 KiB
Dart
523 lines
16 KiB
Dart
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/l10n/l10n.dart';
|
|
import 'package:fl_clash/models/models.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/views/config/on_demand.dart';
|
|
import 'package:fl_clash/views/config/user_agents.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
part 'general/port_dialog.dart';
|
|
|
|
class LogLevelItem extends ConsumerWidget {
|
|
const LogLevelItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
return ConfigOptionsItem<LogLevel>(
|
|
title: (l) => l.logLevel,
|
|
options: LogLevel.values,
|
|
textBuilder: (logLevel) => logLevel.name,
|
|
selector: patchClashConfigProvider.select((state) => state.logLevel),
|
|
onChanged: (ref, value) => ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.update((state) => state.copyWith(logLevel: value)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class UaItem extends ConsumerWidget {
|
|
const UaItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final appLocalizations = context.appLocalizations;
|
|
final globalUa = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.globalUa),
|
|
);
|
|
return ListItem.open(
|
|
title: Text(appLocalizations.userAgent),
|
|
subtitle: Text(globalUa ?? appLocalizations.defaultText),
|
|
widget: const UserAgentsView(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class KeepAliveIntervalItem extends ConsumerWidget {
|
|
const KeepAliveIntervalItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final appLocalizations = context.appLocalizations;
|
|
final keepAliveInterval = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.keepAliveInterval),
|
|
);
|
|
return ListItem.input(
|
|
title: Text(appLocalizations.keepAliveIntervalDesc),
|
|
subtitle: Text(appLocalizations.secondsCount(keepAliveInterval)),
|
|
dialogTitle: appLocalizations.keepAliveIntervalDesc,
|
|
suffixText: appLocalizations.seconds,
|
|
resetValue: '$defaultKeepAliveInterval',
|
|
value: '$keepAliveInterval',
|
|
maxLength: TextInputLimits.interval,
|
|
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)
|
|
.update((state) => state.copyWith(keepAliveInterval: intValue));
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class TestUrlItem extends ConsumerWidget {
|
|
const TestUrlItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final appLocalizations = context.appLocalizations;
|
|
final testUrl = ref.watch(
|
|
appSettingProvider.select((state) => state.testUrl),
|
|
);
|
|
return ListItem.input(
|
|
title: Text(appLocalizations.testUrl),
|
|
subtitle: Text(testUrl),
|
|
resetValue: defaultTestUrl,
|
|
dialogTitle: appLocalizations.testUrl,
|
|
value: testUrl,
|
|
maxLength: TextInputLimits.url,
|
|
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)
|
|
.update((state) => state.copyWith(testUrl: value));
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class PortItem extends ConsumerWidget {
|
|
const PortItem({super.key});
|
|
|
|
Future<void> handleShowPortDialog() async {
|
|
await dialogs.showCommonDialog(child: const _PortDialog());
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final appLocalizations = context.appLocalizations;
|
|
final mixedPort = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.mixedPort),
|
|
);
|
|
return ListItem(
|
|
title: Text(appLocalizations.port),
|
|
subtitle: Text('$mixedPort'),
|
|
onTap: () {
|
|
handleShowPortDialog();
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class HostsItem extends ConsumerWidget {
|
|
const HostsItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final hosts = ref.watch(
|
|
patchClashConfigProvider.select((state) => state.hosts),
|
|
);
|
|
return ListItem.open(
|
|
title: const Text('Hosts'),
|
|
widget: MapEditView(
|
|
title: 'Hosts',
|
|
entries: hosts,
|
|
keyMaxLength: TextInputLimits.domain,
|
|
valueMaxLength: TextInputLimits.hostValue,
|
|
titleBuilder: (item) => Text(item.key),
|
|
subtitleBuilder: (item) => Text(item.value),
|
|
),
|
|
onChanged: (value) {
|
|
ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.update((state) => state.copyWith(hosts: value));
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class AuthenticationItem extends ConsumerWidget {
|
|
const AuthenticationItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
return ConfigToggleItem(
|
|
title: (l) => l.authentication,
|
|
subtitle: (l) => l.authenticationDesc,
|
|
selector: networkSettingProvider.select(
|
|
(state) => state.authentication.enable,
|
|
),
|
|
onChanged: (ref, value) =>
|
|
ref.read(networkSettingProvider.notifier).update((state) {
|
|
var authentication = state.authentication.copyWith(enable: value);
|
|
if (value && authentication.username.isEmpty) {
|
|
authentication = authentication.copyWith(
|
|
username: generateRandomSecret(8),
|
|
password: generateRandomSecret(16),
|
|
);
|
|
}
|
|
return state.copyWith(authentication: authentication);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AuthenticationAccountItem extends ConsumerWidget {
|
|
const AuthenticationAccountItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
return ConfigTextItem(
|
|
title: (l) => l.account,
|
|
maxLength: TextInputLimits.userName,
|
|
selector: networkSettingProvider.select(
|
|
(state) => state.authentication.username,
|
|
),
|
|
// mihomo and the Dart proxy string both split user:pass on the first
|
|
// colon, so a colon in the username breaks authentication.
|
|
normalize: (value) => value.trim().replaceAll(':', ''),
|
|
onChanged: (ref, value) => ref
|
|
.read(networkSettingProvider.notifier)
|
|
.update((state) => state.copyWith.authentication(username: value)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AuthenticationPasswordItem extends ConsumerWidget {
|
|
const AuthenticationPasswordItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
return ConfigTextItem(
|
|
title: (l) => l.password,
|
|
maxLength: TextInputLimits.password,
|
|
selector: networkSettingProvider.select(
|
|
(state) => state.authentication.password,
|
|
),
|
|
normalize: (value) => value.trim(),
|
|
onChanged: (ref, value) => ref
|
|
.read(networkSettingProvider.notifier)
|
|
.update((state) => state.copyWith.authentication(password: value)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ExternalControllerSecretItem extends ConsumerWidget {
|
|
const ExternalControllerSecretItem({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
return ConfigTextItem(
|
|
title: (l) => l.externalControllerSecret,
|
|
maxLength: TextInputLimits.password,
|
|
selector: patchClashConfigProvider.select((state) => state.secret),
|
|
normalize: (value) => value.trim(),
|
|
onChanged: (ref, value) => ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.update((state) => state.copyWith(secret: value)),
|
|
);
|
|
}
|
|
}
|
|
|
|
ConfigToggleItem _clashToggle({
|
|
required ConfigLabel title,
|
|
ConfigLabel? subtitle,
|
|
required bool Function(PatchClashConfig state) select,
|
|
required PatchClashConfig Function(PatchClashConfig state, bool value) update,
|
|
}) {
|
|
return ConfigToggleItem(
|
|
title: title,
|
|
subtitle: subtitle,
|
|
selector: patchClashConfigProvider.select(select),
|
|
onChanged: (ref, value) => ref
|
|
.read(patchClashConfigProvider.notifier)
|
|
.update((state) => update(state, value)),
|
|
);
|
|
}
|
|
|
|
ConfigToggleItem _appSettingToggle({
|
|
required ConfigLabel title,
|
|
ConfigLabel? subtitle,
|
|
required bool Function(AppSettingProps state) select,
|
|
required AppSettingProps Function(AppSettingProps state, bool value) update,
|
|
}) {
|
|
return ConfigToggleItem(
|
|
title: title,
|
|
subtitle: subtitle,
|
|
selector: appSettingProvider.select(select),
|
|
onChanged: (ref, value) => ref
|
|
.read(appSettingProvider.notifier)
|
|
.update((state) => update(state, value)),
|
|
);
|
|
}
|
|
|
|
class GeneralView extends ConsumerWidget {
|
|
const GeneralView({super.key});
|
|
|
|
List<Widget> _startupItems(AppLocalizations appLocalizations) {
|
|
return [
|
|
if (system.isDesktop) ...[
|
|
_appSettingToggle(
|
|
title: (l) => l.autoLaunch,
|
|
subtitle: (l) => l.autoLaunchDesc,
|
|
select: (state) => state.autoLaunch,
|
|
update: (state, value) => state.copyWith(autoLaunch: value),
|
|
),
|
|
_appSettingToggle(
|
|
title: (l) => l.silentLaunch,
|
|
subtitle: (l) => l.silentLaunchDesc,
|
|
select: (state) => state.silentLaunch,
|
|
update: (state, value) => state.copyWith(silentLaunch: value),
|
|
),
|
|
],
|
|
_appSettingToggle(
|
|
title: (l) => l.autoRun,
|
|
subtitle: (l) => l.autoRunDesc,
|
|
select: (state) => state.autoRun,
|
|
update: (state, value) => state.copyWith(autoRun: value),
|
|
),
|
|
ListItem.open(
|
|
title: Text(appLocalizations.onDemand),
|
|
subtitle: Text(appLocalizations.onDemandDesc),
|
|
widget: const OnDemandView(),
|
|
),
|
|
_appSettingToggle(
|
|
title: (l) => l.minimizeOnExit,
|
|
select: (state) => state.minimizeOnExit,
|
|
update: (state, value) => state.copyWith(minimizeOnExit: value),
|
|
),
|
|
if (system.isAndroid) ...[
|
|
_appSettingToggle(
|
|
title: (l) => l.exclude,
|
|
subtitle: (l) => l.excludeDesc,
|
|
select: (state) => state.hidden,
|
|
update: (state, value) => state.copyWith(hidden: value),
|
|
),
|
|
_appSettingToggle(
|
|
title: (l) => l.showNotificationStopAction,
|
|
select: (state) => state.showNotificationStopAction,
|
|
update: (state, value) =>
|
|
state.copyWith(showNotificationStopAction: value),
|
|
),
|
|
],
|
|
];
|
|
}
|
|
|
|
List<Widget> _inboundItems({
|
|
required bool authentication,
|
|
required bool externalController,
|
|
}) {
|
|
return [
|
|
const PortItem(),
|
|
_clashToggle(
|
|
title: (l) => l.allowLan,
|
|
select: (state) => state.allowLan,
|
|
update: (state, value) => state.copyWith(allowLan: value),
|
|
),
|
|
_clashToggle(
|
|
title: (l) => l.externalController,
|
|
subtitle: (l) => l.externalControllerDesc,
|
|
select: (state) =>
|
|
state.externalController == ExternalControllerStatus.open,
|
|
update: (state, value) => state
|
|
.copyWith(
|
|
externalController: value
|
|
? ExternalControllerStatus.open
|
|
: ExternalControllerStatus.close,
|
|
)
|
|
.ensureControllerSecret(),
|
|
),
|
|
if (externalController) const ExternalControllerSecretItem(),
|
|
const AuthenticationItem(),
|
|
if (authentication) ...const [
|
|
AuthenticationAccountItem(),
|
|
AuthenticationPasswordItem(),
|
|
],
|
|
];
|
|
}
|
|
|
|
List<Widget> _connectionItems() {
|
|
return [
|
|
const TestUrlItem(),
|
|
_clashToggle(
|
|
title: (l) => l.unifiedDelay,
|
|
select: (state) => state.unifiedDelay,
|
|
update: (state, value) => state.copyWith(unifiedDelay: value),
|
|
),
|
|
_clashToggle(
|
|
title: (l) => l.tcpConcurrent,
|
|
select: (state) => state.tcpConcurrent,
|
|
update: (state, value) => state.copyWith(tcpConcurrent: value),
|
|
),
|
|
if (system.isDesktop) const KeepAliveIntervalItem(),
|
|
_clashToggle(
|
|
title: (l) => l.findProcessMode,
|
|
select: (state) => state.findProcessMode == FindProcessMode.always,
|
|
update: (state, value) => state.copyWith(
|
|
findProcessMode: value ? FindProcessMode.always : FindProcessMode.off,
|
|
),
|
|
),
|
|
_appSettingToggle(
|
|
title: (l) => l.autoCloseConnections,
|
|
subtitle: (l) => l.autoCloseConnectionsDesc,
|
|
select: (state) => state.closeConnections,
|
|
update: (state, value) => state.copyWith(closeConnections: value),
|
|
),
|
|
_appSettingToggle(
|
|
title: (l) => l.onlyStatisticsProxy,
|
|
select: (state) => state.onlyStatisticsProxy,
|
|
update: (state, value) => state.copyWith(onlyStatisticsProxy: value),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _requestItems() {
|
|
return [
|
|
const UaItem(),
|
|
_appSettingToggle(
|
|
title: (l) => l.checkCertificate,
|
|
subtitle: (l) => l.checkCertificateDesc,
|
|
select: (state) => state.checkCertificate,
|
|
update: (state, value) => state.copyWith(checkCertificate: value),
|
|
),
|
|
_appSettingToggle(
|
|
title: (l) => l.autoCheckUpdate,
|
|
select: (state) => state.autoCheckUpdate,
|
|
update: (state, value) => state.copyWith(autoCheckUpdate: value),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _coreItems() {
|
|
return [
|
|
_clashToggle(
|
|
title: (l) => 'IPv6',
|
|
subtitle: (l) => l.ipv6Desc,
|
|
select: (state) => state.ipv6,
|
|
update: (state, value) => state.copyWith(ipv6: value),
|
|
),
|
|
const HostsItem(),
|
|
ConfigToggleItem(
|
|
title: (l) => l.appendSystemDns,
|
|
selector: networkSettingProvider.select(
|
|
(state) => state.appendSystemDns,
|
|
),
|
|
onChanged: (ref, value) => ref
|
|
.read(networkSettingProvider.notifier)
|
|
.update((state) => state.copyWith(appendSystemDns: value)),
|
|
),
|
|
_clashToggle(
|
|
title: (l) => l.geodataLoader,
|
|
select: (state) => state.geodataLoader == GeodataLoader.memconservative,
|
|
update: (state, value) => state.copyWith(
|
|
geodataLoader: value
|
|
? GeodataLoader.memconservative
|
|
: GeodataLoader.standard,
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _logItems() {
|
|
return [
|
|
const LogLevelItem(),
|
|
_appSettingToggle(
|
|
title: (l) => l.logcat,
|
|
subtitle: (l) => l.logcatDesc,
|
|
select: (state) => state.openLogs,
|
|
update: (state, value) => state.copyWith(openLogs: value),
|
|
),
|
|
if (system.isAndroid)
|
|
_appSettingToggle(
|
|
title: (l) => l.crashlytics,
|
|
subtitle: (l) => l.crashlyticsTip,
|
|
select: (state) => state.crashlytics,
|
|
update: (state, value) => state.copyWith(crashlytics: value),
|
|
),
|
|
];
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final appLocalizations = context.appLocalizations;
|
|
final authentication = ref.watch(
|
|
networkSettingProvider.select((state) => state.authentication.enable),
|
|
);
|
|
final externalController = ref.watch(
|
|
patchClashConfigProvider.select(
|
|
(state) => state.externalController == ExternalControllerStatus.open,
|
|
),
|
|
);
|
|
return BaseScaffold(
|
|
title: appLocalizations.general,
|
|
body: ListView(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
).copyWith(top: context.appBarInset, bottom: 16),
|
|
children: [
|
|
generateSectionV3(
|
|
title: appLocalizations.startupAndBackground,
|
|
items: _startupItems(appLocalizations),
|
|
),
|
|
generateSectionV3(
|
|
title: appLocalizations.requestsAndUpdates,
|
|
items: _requestItems(),
|
|
),
|
|
generateSectionV3(
|
|
title: appLocalizations.inbound,
|
|
items: _inboundItems(
|
|
authentication: authentication,
|
|
externalController: externalController,
|
|
),
|
|
),
|
|
generateSectionV3(
|
|
title: appLocalizations.connection,
|
|
items: _connectionItems(),
|
|
),
|
|
generateSectionV3(title: appLocalizations.core, items: _coreItems()),
|
|
generateSectionV3(
|
|
title: appLocalizations.logsAndDiagnostics,
|
|
items: _logItems(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|