Optimize ip detection
Support android vpn ipv6 inbound switch Support log export Optimize more details
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class Android {
|
||||
init() async {
|
||||
app?.onExit = () {};
|
||||
app?.onExit = () async {
|
||||
await globalState.appController.savePreferences();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,5 @@ export 'iterable.dart';
|
||||
export 'scroll.dart';
|
||||
export 'icons.dart';
|
||||
export 'http.dart';
|
||||
export 'keyboard.dart';
|
||||
export 'keyboard.dart';
|
||||
export 'network.dart';
|
||||
@@ -26,9 +26,9 @@ const GeoXMap defaultGeoXMap = {
|
||||
"mmdb":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||
"asn":
|
||||
"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
|
||||
"geoip":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat",
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
|
||||
"geosite":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
|
||||
};
|
||||
|
||||
@@ -15,9 +15,6 @@ class FlClashHttpOverrides extends HttpOverrides {
|
||||
final port = appController.clashConfig.mixedPort;
|
||||
final isStart = appController.appFlowingState.isStart;
|
||||
if (!isStart) return "DIRECT";
|
||||
if (appController.appState.groups.isEmpty) {
|
||||
return "DIRECT";
|
||||
}
|
||||
return "PROXY localhost:$port";
|
||||
};
|
||||
return client;
|
||||
|
||||
@@ -15,4 +15,10 @@ extension ListExtension<T> on List<T> {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
List<T> safeSublist(int start) {
|
||||
if(start <= 0) return this;
|
||||
if(start > length) return [];
|
||||
return sublist(start);
|
||||
}
|
||||
}
|
||||
|
||||
25
lib/common/network.dart
Normal file
25
lib/common/network.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
|
||||
extension NetworkInterfaceExt on NetworkInterface {
|
||||
bool get isWifi {
|
||||
final nameLowCase = name.toLowerCase();
|
||||
if (nameLowCase.contains('wlan') ||
|
||||
nameLowCase.contains('wi-fi') ||
|
||||
nameLowCase == 'en0' ||
|
||||
nameLowCase == 'eth0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get includesIPv4 {
|
||||
return addresses.any((addr) => addr.isIPv4);
|
||||
}
|
||||
}
|
||||
|
||||
extension InternetAddressExt on InternetAddress {
|
||||
bool get isIPv4 {
|
||||
return type == InternetAddressType.IPv4;
|
||||
}
|
||||
}
|
||||
@@ -171,14 +171,18 @@ class Other {
|
||||
if (disposition == null) return null;
|
||||
final parseValue = HeaderValue.parse(disposition);
|
||||
final parameters = parseValue.parameters;
|
||||
final key = parameters.keys
|
||||
.firstWhere((key) => key.startsWith("filename"), orElse: () => '');
|
||||
if (key.isEmpty) return null;
|
||||
if (key == "filename*") {
|
||||
return Uri.decodeComponent((parameters[key] ?? "").split("'").last);
|
||||
} else {
|
||||
return parameters[key];
|
||||
final fileNamePointKey = parameters.keys
|
||||
.firstWhere((key) => key == "filename*", orElse: () => "");
|
||||
if (fileNamePointKey.isNotEmpty) {
|
||||
final res = parameters[fileNamePointKey]?.split("''") ?? [];
|
||||
if (res.length >= 2) {
|
||||
return Uri.decodeComponent(res[1]);
|
||||
}
|
||||
}
|
||||
final fileNameKey = parameters.keys
|
||||
.firstWhere((key) => key == "filename", orElse: () => "");
|
||||
if (fileNameKey.isEmpty) return null;
|
||||
return parameters[fileNameKey];
|
||||
}
|
||||
|
||||
double getViewWidth() {
|
||||
@@ -221,11 +225,14 @@ class Other {
|
||||
return "${appName}_backup_${DateTime.now().show}.zip";
|
||||
}
|
||||
|
||||
String get logFile {
|
||||
return "${appName}_${DateTime.now().show}.log";
|
||||
}
|
||||
|
||||
Size getScreenSize() {
|
||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
return view.physicalSize / view.devicePixelRatio;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final other = Other();
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'window.dart';
|
||||
|
||||
@@ -2,10 +2,12 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:fl_clash/common/archive.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart';
|
||||
@@ -58,7 +60,9 @@ class AppController {
|
||||
updateRunTime,
|
||||
updateTraffic,
|
||||
];
|
||||
applyProfileDebounce();
|
||||
if (!Platform.isAndroid) {
|
||||
applyProfileDebounce();
|
||||
}
|
||||
} else {
|
||||
await globalState.handleStop();
|
||||
clashCore.resetTraffic();
|
||||
@@ -118,11 +122,15 @@ class AppController {
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||
await globalState.updateClashConfig(
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
isPatch: isPatch,
|
||||
);
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.updateClashConfig(
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
isPatch: isPatch,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future applyProfile({bool isPrue = false}) async {
|
||||
@@ -299,7 +307,7 @@ class AppController {
|
||||
init() async {
|
||||
final isDisclaimerAccepted = await handlerDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
system.exit();
|
||||
handleExit();
|
||||
}
|
||||
updateLogStatus();
|
||||
if (!config.silentLaunch) {
|
||||
@@ -520,21 +528,6 @@ class AppController {
|
||||
'';
|
||||
}
|
||||
|
||||
Future<List<int>> backupData() async {
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
final profilesPath = await appPath.getProfilesPath();
|
||||
final configJson = config.toJson();
|
||||
final clashConfigJson = clashConfig.toJson();
|
||||
return Isolate.run<List<int>>(() async {
|
||||
final archive = Archive();
|
||||
archive.add("config.json", configJson);
|
||||
archive.add("clashConfig.json", clashConfigJson);
|
||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||
final zipEncoder = ZipEncoder();
|
||||
return zipEncoder.encode(archive) ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
@@ -573,6 +566,36 @@ class AppController {
|
||||
clashConfig.mode = Mode.values[nextIndex];
|
||||
}
|
||||
|
||||
Future<bool> exportLogs() async {
|
||||
final logsRaw = appFlowingState.logs.map(
|
||||
(item) => item.toString(),
|
||||
);
|
||||
final data = await Isolate.run<List<int>>(() async {
|
||||
final logsRawString = logsRaw.join("\n");
|
||||
return utf8.encode(logsRawString);
|
||||
});
|
||||
return await picker.saveFile(
|
||||
other.logFile,
|
||||
Uint8List.fromList(data),
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
|
||||
Future<List<int>> backupData() async {
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
final profilesPath = await appPath.getProfilesPath();
|
||||
final configJson = config.toJson();
|
||||
final clashConfigJson = clashConfig.toJson();
|
||||
return Isolate.run<List<int>>(() async {
|
||||
final archive = Archive();
|
||||
archive.add("config.json", configJson);
|
||||
archive.add("clashConfig.json", clashConfigJson);
|
||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||
final zipEncoder = ZipEncoder();
|
||||
return zipEncoder.encode(archive) ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
recoveryData(
|
||||
List<int> data,
|
||||
RecoveryOption recoveryOption,
|
||||
|
||||
@@ -15,6 +15,10 @@ extension GroupTypeExtension on GroupType {
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool get isURLTestOrFallback {
|
||||
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
||||
}
|
||||
|
||||
static GroupType? getGroupType(String value) {
|
||||
final index = GroupTypeExtension.valueList.indexOf(value);
|
||||
if (index == -1) return null;
|
||||
|
||||
@@ -67,11 +67,9 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
||||
title: const Text("DNS"),
|
||||
subtitle: Text(appLocalizations.dnsDesc),
|
||||
leading: const Icon(Icons.dns),
|
||||
delegate: OpenDelegate(
|
||||
delegate: const OpenDelegate(
|
||||
title: "DNS",
|
||||
widget: generateListView(
|
||||
dnsItems,
|
||||
),
|
||||
widget: DnsListView(),
|
||||
isScaffold: true,
|
||||
isBlur: false,
|
||||
extendPageWidth: 360,
|
||||
|
||||
@@ -10,35 +10,8 @@ import 'package:provider/provider.dart';
|
||||
class OverrideItem extends StatelessWidget {
|
||||
const OverrideItem({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.resetDns,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.dnsResetTip,
|
||||
),
|
||||
onTab: () {
|
||||
globalState.appController.clashConfig.dns = const Dns();
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
},
|
||||
tooltip: appLocalizations.resetDns,
|
||||
icon: const Icon(
|
||||
Icons.replay,
|
||||
),
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.overrideDns,
|
||||
builder: (_, override, __) {
|
||||
@@ -58,35 +31,6 @@ class OverrideItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DnsDisabledContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const DnsDisabledContainer(
|
||||
this.child, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.overrideDns,
|
||||
builder: (_, enable, child) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !enable,
|
||||
child: DisabledMask(
|
||||
status: !enable,
|
||||
child: Container(
|
||||
color: context.colorScheme.surface,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusItem extends StatelessWidget {
|
||||
const StatusItem({super.key});
|
||||
|
||||
@@ -469,7 +413,6 @@ class NameserverPolicyItem extends StatelessWidget {
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
isMap: true,
|
||||
onRemove: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
@@ -795,27 +738,25 @@ class DnsOptions extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DnsDisabledContainer(
|
||||
Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.options,
|
||||
items: [
|
||||
const StatusItem(),
|
||||
const UseHostsItem(),
|
||||
const UseSystemHostsItem(),
|
||||
const IPv6Item(),
|
||||
const RespectRulesItem(),
|
||||
const PreferH3Item(),
|
||||
const DnsModeItem(),
|
||||
const FakeIpRangeItem(),
|
||||
const FakeIpFilterItem(),
|
||||
const DefaultNameserverItem(),
|
||||
const NameserverPolicyItem(),
|
||||
const NameserverItem(),
|
||||
const FallbackItem(),
|
||||
const ProxyServerNameserverItem(),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.options,
|
||||
items: [
|
||||
const StatusItem(),
|
||||
const UseHostsItem(),
|
||||
const UseSystemHostsItem(),
|
||||
const IPv6Item(),
|
||||
const RespectRulesItem(),
|
||||
const PreferH3Item(),
|
||||
const DnsModeItem(),
|
||||
const FakeIpRangeItem(),
|
||||
const FakeIpFilterItem(),
|
||||
const DefaultNameserverItem(),
|
||||
const NameserverPolicyItem(),
|
||||
const NameserverItem(),
|
||||
const FallbackItem(),
|
||||
const ProxyServerNameserverItem(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -826,18 +767,16 @@ class FallbackFilterOptions extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DnsDisabledContainer(
|
||||
Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.fallbackFilter,
|
||||
items: [
|
||||
const GeoipItem(),
|
||||
const GeoipCodeItem(),
|
||||
const GeositeItem(),
|
||||
const IpcidrItem(),
|
||||
const DomainItem(),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
children: generateSection(
|
||||
title: appLocalizations.fallbackFilter,
|
||||
items: [
|
||||
const GeoipItem(),
|
||||
const GeoipCodeItem(),
|
||||
const GeositeItem(),
|
||||
const IpcidrItem(),
|
||||
const DomainItem(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -848,3 +787,41 @@ const dnsItems = <Widget>[
|
||||
DnsOptions(),
|
||||
FallbackFilterOptions(),
|
||||
];
|
||||
|
||||
class DnsListView extends StatelessWidget {
|
||||
const DnsListView({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.resetDns,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.dnsResetTip,
|
||||
),
|
||||
onTab: () {
|
||||
globalState.appController.clashConfig.dns = const Dns();
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
},
|
||||
tooltip: appLocalizations.resetDns,
|
||||
icon: const Icon(
|
||||
Icons.replay,
|
||||
),
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
return generateListView(
|
||||
dnsItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,6 @@ class HostsItem extends StatelessWidget {
|
||||
clashConfig.hosts = Map.from(clashConfig.hosts)
|
||||
..addEntries([value]);
|
||||
},
|
||||
isMap: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -115,6 +115,34 @@ class SystemProxySwitch extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class Ipv6Switch extends StatelessWidget {
|
||||
const Ipv6Switch({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.water_outlined),
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
ipv6: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VpnOptions extends StatelessWidget {
|
||||
const VpnOptions({super.key});
|
||||
|
||||
@@ -127,6 +155,7 @@ class VpnOptions extends StatelessWidget {
|
||||
items: [
|
||||
const SystemProxySwitch(),
|
||||
const AllowBypassSwitch(),
|
||||
const Ipv6Switch(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -13,16 +13,55 @@ class IntranetIP extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _IntranetIPState extends State<IntranetIP> {
|
||||
final ipNotifier = ValueNotifier<String>("");
|
||||
final ipNotifier = ValueNotifier<String?>("");
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list();
|
||||
for (final interface in interfaces) {
|
||||
for (final address in interface.addresses) {
|
||||
if (!address.isLoopback) {
|
||||
return address.address;
|
||||
Future<String> getNetworkType() async {
|
||||
try {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
type: InternetAddressType.any,
|
||||
);
|
||||
|
||||
for (var interface in interfaces) {
|
||||
if (interface.name.toLowerCase().contains('wlan') ||
|
||||
interface.name.toLowerCase().contains('wi-fi')) {
|
||||
return 'WiFi';
|
||||
}
|
||||
if (interface.name.toLowerCase().contains('rmnet') ||
|
||||
interface.name.toLowerCase().contains('ccmni') ||
|
||||
interface.name.toLowerCase().contains('cellular')) {
|
||||
return 'Mobile Data';
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
} catch (e) {
|
||||
return 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
)
|
||||
..sort((a, b) {
|
||||
if (a.isWifi && !b.isWifi) return -1;
|
||||
if (!a.isWifi && b.isWifi) return 1;
|
||||
if (a.includesIPv4 && !b.includesIPv4) return -1;
|
||||
if (!a.includesIPv4 && b.includesIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
for (final interface in interfaces) {
|
||||
final addresses = interface.addresses;
|
||||
if (addresses.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
addresses.sort((a, b) {
|
||||
if (a.isIPv4 && !b.isIPv4) return -1;
|
||||
if (!a.isIPv4 && b.isIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
return addresses.first.address;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -48,17 +87,15 @@ class _IntranetIPState extends State<IntranetIP> {
|
||||
label: appLocalizations.intranetIP,
|
||||
iconData: Icons.devices,
|
||||
),
|
||||
onPressed: (){
|
||||
|
||||
},
|
||||
onPressed: () {},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
height: globalState.measure.titleLargeHeight + 24 - 2,
|
||||
height: globalState.measure.titleMediumHeight + 24 - 2,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: ipNotifier,
|
||||
builder: (_, value, __) {
|
||||
return FadeBox(
|
||||
child: value.isNotEmpty
|
||||
child: value != null
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -67,8 +104,9 @@ class _IntranetIPState extends State<IntranetIP> {
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
value,
|
||||
style: context.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||
value.isNotEmpty ? value : appLocalizations.noNetwork,
|
||||
style: context
|
||||
.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
@@ -22,6 +24,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
);
|
||||
bool? _preIsStart;
|
||||
Function? _checkIpDebounce;
|
||||
Timer? _setTimeoutTimer;
|
||||
CancelToken? cancelToken;
|
||||
|
||||
_checkIp() async {
|
||||
@@ -31,6 +34,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
if (!isInit) return;
|
||||
final isStart = appFlowingState.isStart;
|
||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||
_clearSetTimeoutTimer();
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
@@ -43,11 +47,32 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
cancelToken = CancelToken();
|
||||
try {
|
||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
||||
if (ipInfo != null) {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_setTimeoutTimer = Timer(const Duration(milliseconds: 2000), () {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: null,
|
||||
);
|
||||
});
|
||||
} catch (_) {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
_clearSetTimeoutTimer() {
|
||||
if(_setTimeoutTimer != null){
|
||||
_setTimeoutTimer?.cancel();
|
||||
_setTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_checkIpContainer(Widget child) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -30,18 +31,24 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
logsNotifier.value = logsNotifier.value.copyWith(logs: appFlowingState.logs);
|
||||
logsNotifier.value =
|
||||
logsNotifier.value.copyWith(logs: appFlowingState.logs);
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
final logs = appFlowingState.logs;
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
final logs = appFlowingState.logs.safeSublist(
|
||||
appFlowingState.logs.length - maxLength,
|
||||
);
|
||||
if (!const ListEquality<Log>().equals(
|
||||
logsNotifier.value.logs,
|
||||
logs,
|
||||
)) {
|
||||
logsNotifier.value = logsNotifier.value.copyWith(logs: logs);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
logs: logs,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -56,6 +63,21 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
timer = null;
|
||||
}
|
||||
|
||||
_handleExport() async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
return await globalState.appController.exportLogs();
|
||||
},
|
||||
title: appLocalizations.exportLogs,
|
||||
);
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.exportSuccess),
|
||||
);
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final commonScaffoldState =
|
||||
@@ -72,6 +94,17 @@ class _LogsFragmentState extends State<LogsFragment> {
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_handleExport();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.file_download_outlined,
|
||||
),
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -236,7 +269,8 @@ class LogsSearchDelegate extends SearchDelegate {
|
||||
_addKeyword(String keyword) {
|
||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||
if (isContains) return;
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)..add(keyword);
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||
..add(keyword);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
@@ -245,7 +279,8 @@ class LogsSearchDelegate extends SearchDelegate {
|
||||
_deleteKeyword(String keyword) {
|
||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||
if (!isContains) return;
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)..remove(keyword);
|
||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||
..remove(keyword);
|
||||
logsNotifier.value = logsNotifier.value.copyWith(
|
||||
keywords: keywords,
|
||||
);
|
||||
@@ -339,7 +374,9 @@ class _LogItemState extends State<LogItem> {
|
||||
style: context.textTheme.bodySmall
|
||||
?.copyWith(color: context.colorScheme.primary),
|
||||
),
|
||||
const SizedBox(height: 8,),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CommonChip(
|
||||
|
||||
@@ -102,12 +102,12 @@ class ProxyCard extends StatelessWidget {
|
||||
|
||||
_changeProxy(BuildContext context) async {
|
||||
final appController = globalState.appController;
|
||||
final isUrlTest = groupType == GroupType.URLTest;
|
||||
final isURLTestOrFallback = groupType.isURLTestOrFallback;
|
||||
final isSelector = groupType == GroupType.Selector;
|
||||
if (isUrlTest || isSelector) {
|
||||
if (isURLTestOrFallback || isSelector) {
|
||||
final currentProxyName =
|
||||
appController.config.currentSelectedMap[groupName];
|
||||
final nextProxyName = switch (isUrlTest) {
|
||||
final nextProxyName = switch (isURLTestOrFallback) {
|
||||
true => currentProxyName == proxy.name ? "" : proxy.name,
|
||||
false => proxy.name,
|
||||
};
|
||||
@@ -207,7 +207,7 @@ class ProxyCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (groupType == GroupType.URLTest)
|
||||
if (groupType.isURLTestOrFallback)
|
||||
Selector<Config, String>(
|
||||
selector: (_, config) {
|
||||
final selectedProxyName =
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -37,7 +38,10 @@ class _RequestsFragmentState extends State<RequestsFragment> {
|
||||
timer = null;
|
||||
}
|
||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
final requests = appState.requests;
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
final requests = appState.requests.safeSublist(
|
||||
appState.requests.length - maxLength,
|
||||
);
|
||||
if (!const ListEquality<Connection>().equals(
|
||||
requestsNotifier.value.connections,
|
||||
requests,
|
||||
|
||||
@@ -70,7 +70,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
final isDisclaimerAccepted =
|
||||
await globalState.appController.showDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
system.exit();
|
||||
globalState.appController.handleExit();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"tabAnimation": "Tab animation",
|
||||
"tabAnimationDesc": "When enabled, the home tab will add a toggle animation",
|
||||
"desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
|
||||
"startVpn": "Staring VPN...",
|
||||
"startVpn": "Starting VPN...",
|
||||
"stopVpn": "Stopping VPN...",
|
||||
"discovery": "Discovery a new version",
|
||||
"compatible": "Compatibility mode",
|
||||
@@ -304,5 +304,9 @@
|
||||
"hotkeyConflict": "Hotkey conflict",
|
||||
"remove": "Remove",
|
||||
"noHotKey": "No HotKey",
|
||||
"dnsResetTip": "Make sure to reset the DNS"
|
||||
"dnsResetTip": "Make sure to reset the DNS",
|
||||
"noNetwork": "No network",
|
||||
"ipv6InboundDesc": "Allow IPv6 inbound",
|
||||
"exportLogs": "Export logs",
|
||||
"exportSuccess": "Export Success"
|
||||
}
|
||||
@@ -304,5 +304,9 @@
|
||||
"hotkeyConflict": "快捷键冲突",
|
||||
"remove": "移除",
|
||||
"noHotKey": "暂无快捷键",
|
||||
"dnsResetTip": "确定重置DNS"
|
||||
"dnsResetTip": "确定重置DNS",
|
||||
"noNetwork": "无网络",
|
||||
"ipv6InboundDesc": "允许IPv6入站",
|
||||
"exportLogs": "导出日志",
|
||||
"exportSuccess": "导出成功"
|
||||
}
|
||||
@@ -163,6 +163,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||
"expirationTime":
|
||||
MessageLookupByLibrary.simpleMessage("Expiration time"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("Export logs"),
|
||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("Export Success"),
|
||||
"externalController":
|
||||
MessageLookupByLibrary.simpleMessage("ExternalController"),
|
||||
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -219,6 +221,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"),
|
||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
|
||||
"When turned on it will be able to receive IPv6 traffic"),
|
||||
"ipv6InboundDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Allow IPv6 inbound"),
|
||||
"just": MessageLookupByLibrary.simpleMessage("Just"),
|
||||
"keepAliveIntervalDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
|
||||
@@ -269,6 +273,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"noHotKey": MessageLookupByLibrary.simpleMessage("No HotKey"),
|
||||
"noInfo": MessageLookupByLibrary.simpleMessage("No info"),
|
||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"),
|
||||
"noNetwork": MessageLookupByLibrary.simpleMessage("No network"),
|
||||
"noProxy": MessageLookupByLibrary.simpleMessage("No proxy"),
|
||||
"noProxyDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Please create a profile or add a valid profile"),
|
||||
@@ -395,7 +400,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"source": MessageLookupByLibrary.simpleMessage("Source"),
|
||||
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
||||
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
|
||||
"startVpn": MessageLookupByLibrary.simpleMessage("Starting VPN..."),
|
||||
"status": MessageLookupByLibrary.simpleMessage("Status"),
|
||||
"statusDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"System DNS will be used when turned off"),
|
||||
|
||||
@@ -133,6 +133,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
||||
"expand": MessageLookupByLibrary.simpleMessage("标准"),
|
||||
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
|
||||
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
||||
"externalControllerDesc":
|
||||
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
|
||||
@@ -174,6 +176,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
|
||||
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
|
||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
|
||||
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
|
||||
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
||||
"keepAliveIntervalDesc":
|
||||
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
|
||||
@@ -214,6 +217,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
|
||||
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
|
||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
|
||||
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
|
||||
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
||||
"noProxyDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
|
||||
|
||||
@@ -1290,10 +1290,10 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Staring VPN...`
|
||||
/// `Starting VPN...`
|
||||
String get startVpn {
|
||||
return Intl.message(
|
||||
'Staring VPN...',
|
||||
'Starting VPN...',
|
||||
name: 'startVpn',
|
||||
desc: '',
|
||||
args: [],
|
||||
@@ -3109,6 +3109,46 @@ class AppLocalizations {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No network`
|
||||
String get noNetwork {
|
||||
return Intl.message(
|
||||
'No network',
|
||||
name: 'noNetwork',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Allow IPv6 inbound`
|
||||
String get ipv6InboundDesc {
|
||||
return Intl.message(
|
||||
'Allow IPv6 inbound',
|
||||
name: 'ipv6InboundDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Export logs`
|
||||
String get exportLogs {
|
||||
return Intl.message(
|
||||
'Export logs',
|
||||
name: 'exportLogs',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Export Success`
|
||||
String get exportSuccess {
|
||||
return Intl.message(
|
||||
'Export Success',
|
||||
name: 'exportSuccess',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
|
||||
@@ -18,6 +18,7 @@ class AppStateManager extends StatefulWidget {
|
||||
|
||||
class _AppStateManagerState extends State<AppStateManager>
|
||||
with WidgetsBindingObserver {
|
||||
|
||||
_updateNavigationsContainer(Widget child) {
|
||||
return Selector2<AppState, Config, UpdateNavigationsSelector>(
|
||||
selector: (_, appState, config) {
|
||||
|
||||
@@ -19,9 +19,9 @@ class ClashManager extends StatefulWidget {
|
||||
State<ClashManager> createState() => _ClashContainerState();
|
||||
}
|
||||
|
||||
class _ClashContainerState extends State<ClashManager>
|
||||
with AppMessageListener {
|
||||
class _ClashContainerState extends State<ClashManager> with AppMessageListener {
|
||||
Function? updateClashConfigDebounce;
|
||||
Function? updateDelayDebounce;
|
||||
|
||||
Widget _updateContainer(Widget child) {
|
||||
return Selector2<Config, ClashConfig, ClashConfigState>(
|
||||
@@ -66,6 +66,7 @@ class _ClashContainerState extends State<ClashManager>
|
||||
selector: (_, config, clashConfig) => CoreState(
|
||||
accessControl: config.isAccessControl ? config.accessControl : null,
|
||||
enable: config.vpnProps.enable,
|
||||
ipv6: config.vpnProps.ipv6,
|
||||
allowBypass: config.vpnProps.allowBypass,
|
||||
systemProxy: config.vpnProps.systemProxy,
|
||||
mixedPort: clashConfig.mixedPort,
|
||||
@@ -132,7 +133,11 @@ class _ClashContainerState extends State<ClashManager>
|
||||
final appController = globalState.appController;
|
||||
appController.setDelay(delay);
|
||||
super.onDelay(delay);
|
||||
await globalState.appController.updateGroupDebounce();
|
||||
updateDelayDebounce ??= debounce(() async {
|
||||
await appController.updateGroupDebounce();
|
||||
await appController.addCheckIpNumDebounce();
|
||||
});
|
||||
updateDelayDebounce!();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -145,6 +150,12 @@ class _ClashContainerState extends State<ClashManager>
|
||||
super.onLog(log);
|
||||
}
|
||||
|
||||
@override
|
||||
void onStarted(String runTime) {
|
||||
super.onStarted(runTime);
|
||||
globalState.appController.applyProfileDebounce();
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(Connection connection) async {
|
||||
globalState.appController.appState.addRequest(connection);
|
||||
@@ -159,7 +170,7 @@ class _ClashContainerState extends State<ClashManager>
|
||||
providerName,
|
||||
),
|
||||
);
|
||||
appController.addCheckIpNumDebounce();
|
||||
// appController.addCheckIpNumDebounce();
|
||||
super.onLoaded(providerName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +151,8 @@ class AppState with ChangeNotifier {
|
||||
|
||||
addRequest(Connection value) {
|
||||
_requests = List.from(_requests)..add(value);
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
if (_requests.length > maxLength) {
|
||||
_requests = _requests.sublist(_requests.length - maxLength);
|
||||
}
|
||||
const maxLength = 1000;
|
||||
_requests = _requests.safeSublist(_requests.length - maxLength);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -354,10 +352,8 @@ class AppFlowingState with ChangeNotifier {
|
||||
|
||||
addLog(Log log) {
|
||||
_logs = List.from(_logs)..add(log);
|
||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||
if (_logs.length > maxLength) {
|
||||
_logs = _logs.sublist(_logs.length - maxLength);
|
||||
}
|
||||
const maxLength = 1000;
|
||||
_logs = _logs.safeSublist(_logs.length - maxLength);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -373,9 +369,7 @@ class AppFlowingState with ChangeNotifier {
|
||||
addTraffic(Traffic traffic) {
|
||||
_traffics = List.from(_traffics)..add(traffic);
|
||||
const maxLength = 60;
|
||||
if (_traffics.length > maxLength) {
|
||||
_traffics = _traffics.sublist(_traffics.length - maxLength);
|
||||
}
|
||||
_traffics = _traffics.safeSublist(_traffics.length - maxLength);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ extension GroupExt on Group {
|
||||
String get realNow => now ?? "";
|
||||
|
||||
String getCurrentSelectedName(String proxyName) {
|
||||
if (type == GroupType.URLTest) {
|
||||
if (type.isURLTestOrFallback) {
|
||||
return realNow.isNotEmpty ? realNow : proxyName;
|
||||
}
|
||||
return proxyName.isNotEmpty ? proxyName : realNow;
|
||||
|
||||
@@ -42,6 +42,7 @@ class CoreState with _$CoreState {
|
||||
required bool allowBypass,
|
||||
required bool systemProxy,
|
||||
required int mixedPort,
|
||||
required bool ipv6,
|
||||
required bool onlyProxy,
|
||||
}) = _CoreState;
|
||||
|
||||
@@ -77,7 +78,8 @@ class WindowProps with _$WindowProps {
|
||||
class VpnProps with _$VpnProps {
|
||||
const factory VpnProps({
|
||||
@Default(true) bool enable,
|
||||
@Default(false) bool systemProxy,
|
||||
@Default(true) bool systemProxy,
|
||||
@Default(false) bool ipv6,
|
||||
@Default(true) bool allowBypass,
|
||||
}) = _VpnProps;
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
|
||||
'mmdb':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
|
||||
'asn':
|
||||
'https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb',
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb',
|
||||
'geoip':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat',
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat',
|
||||
'geosite':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
|
||||
}
|
||||
|
||||
@@ -274,6 +274,7 @@ mixin _$CoreState {
|
||||
bool get allowBypass => throw _privateConstructorUsedError;
|
||||
bool get systemProxy => throw _privateConstructorUsedError;
|
||||
int get mixedPort => throw _privateConstructorUsedError;
|
||||
bool get ipv6 => throw _privateConstructorUsedError;
|
||||
bool get onlyProxy => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -294,6 +295,7 @@ abstract class $CoreStateCopyWith<$Res> {
|
||||
bool allowBypass,
|
||||
bool systemProxy,
|
||||
int mixedPort,
|
||||
bool ipv6,
|
||||
bool onlyProxy});
|
||||
|
||||
$AccessControlCopyWith<$Res>? get accessControl;
|
||||
@@ -318,6 +320,7 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
|
||||
Object? allowBypass = null,
|
||||
Object? systemProxy = null,
|
||||
Object? mixedPort = null,
|
||||
Object? ipv6 = null,
|
||||
Object? onlyProxy = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
@@ -345,6 +348,10 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
|
||||
? _value.mixedPort
|
||||
: mixedPort // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
@@ -380,6 +387,7 @@ abstract class _$$CoreStateImplCopyWith<$Res>
|
||||
bool allowBypass,
|
||||
bool systemProxy,
|
||||
int mixedPort,
|
||||
bool ipv6,
|
||||
bool onlyProxy});
|
||||
|
||||
@override
|
||||
@@ -403,6 +411,7 @@ class __$$CoreStateImplCopyWithImpl<$Res>
|
||||
Object? allowBypass = null,
|
||||
Object? systemProxy = null,
|
||||
Object? mixedPort = null,
|
||||
Object? ipv6 = null,
|
||||
Object? onlyProxy = null,
|
||||
}) {
|
||||
return _then(_$CoreStateImpl(
|
||||
@@ -430,6 +439,10 @@ class __$$CoreStateImplCopyWithImpl<$Res>
|
||||
? _value.mixedPort
|
||||
: mixedPort // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
@@ -448,6 +461,7 @@ class _$CoreStateImpl implements _CoreState {
|
||||
required this.allowBypass,
|
||||
required this.systemProxy,
|
||||
required this.mixedPort,
|
||||
required this.ipv6,
|
||||
required this.onlyProxy});
|
||||
|
||||
factory _$CoreStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -466,11 +480,13 @@ class _$CoreStateImpl implements _CoreState {
|
||||
@override
|
||||
final int mixedPort;
|
||||
@override
|
||||
final bool ipv6;
|
||||
@override
|
||||
final bool onlyProxy;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)';
|
||||
return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, enable: $enable, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, ipv6: $ipv6, onlyProxy: $onlyProxy)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -489,6 +505,7 @@ class _$CoreStateImpl implements _CoreState {
|
||||
other.systemProxy == systemProxy) &&
|
||||
(identical(other.mixedPort, mixedPort) ||
|
||||
other.mixedPort == mixedPort) &&
|
||||
(identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
|
||||
(identical(other.onlyProxy, onlyProxy) ||
|
||||
other.onlyProxy == onlyProxy));
|
||||
}
|
||||
@@ -503,6 +520,7 @@ class _$CoreStateImpl implements _CoreState {
|
||||
allowBypass,
|
||||
systemProxy,
|
||||
mixedPort,
|
||||
ipv6,
|
||||
onlyProxy);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@@ -527,6 +545,7 @@ abstract class _CoreState implements CoreState {
|
||||
required final bool allowBypass,
|
||||
required final bool systemProxy,
|
||||
required final int mixedPort,
|
||||
required final bool ipv6,
|
||||
required final bool onlyProxy}) = _$CoreStateImpl;
|
||||
|
||||
factory _CoreState.fromJson(Map<String, dynamic> json) =
|
||||
@@ -545,6 +564,8 @@ abstract class _CoreState implements CoreState {
|
||||
@override
|
||||
int get mixedPort;
|
||||
@override
|
||||
bool get ipv6;
|
||||
@override
|
||||
bool get onlyProxy;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
@@ -934,6 +955,7 @@ VpnProps _$VpnPropsFromJson(Map<String, dynamic> json) {
|
||||
mixin _$VpnProps {
|
||||
bool get enable => throw _privateConstructorUsedError;
|
||||
bool get systemProxy => throw _privateConstructorUsedError;
|
||||
bool get ipv6 => throw _privateConstructorUsedError;
|
||||
bool get allowBypass => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -947,7 +969,7 @@ abstract class $VpnPropsCopyWith<$Res> {
|
||||
factory $VpnPropsCopyWith(VpnProps value, $Res Function(VpnProps) then) =
|
||||
_$VpnPropsCopyWithImpl<$Res, VpnProps>;
|
||||
@useResult
|
||||
$Res call({bool enable, bool systemProxy, bool allowBypass});
|
||||
$Res call({bool enable, bool systemProxy, bool ipv6, bool allowBypass});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -965,6 +987,7 @@ class _$VpnPropsCopyWithImpl<$Res, $Val extends VpnProps>
|
||||
$Res call({
|
||||
Object? enable = null,
|
||||
Object? systemProxy = null,
|
||||
Object? ipv6 = null,
|
||||
Object? allowBypass = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
@@ -976,6 +999,10 @@ class _$VpnPropsCopyWithImpl<$Res, $Val extends VpnProps>
|
||||
? _value.systemProxy
|
||||
: systemProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
allowBypass: null == allowBypass
|
||||
? _value.allowBypass
|
||||
: allowBypass // ignore: cast_nullable_to_non_nullable
|
||||
@@ -992,7 +1019,7 @@ abstract class _$$VpnPropsImplCopyWith<$Res>
|
||||
__$$VpnPropsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({bool enable, bool systemProxy, bool allowBypass});
|
||||
$Res call({bool enable, bool systemProxy, bool ipv6, bool allowBypass});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -1008,6 +1035,7 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? enable = null,
|
||||
Object? systemProxy = null,
|
||||
Object? ipv6 = null,
|
||||
Object? allowBypass = null,
|
||||
}) {
|
||||
return _then(_$VpnPropsImpl(
|
||||
@@ -1019,6 +1047,10 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
|
||||
? _value.systemProxy
|
||||
: systemProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
ipv6: null == ipv6
|
||||
? _value.ipv6
|
||||
: ipv6 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
allowBypass: null == allowBypass
|
||||
? _value.allowBypass
|
||||
: allowBypass // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1031,7 +1063,10 @@ class __$$VpnPropsImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$VpnPropsImpl implements _VpnProps {
|
||||
const _$VpnPropsImpl(
|
||||
{this.enable = true, this.systemProxy = false, this.allowBypass = true});
|
||||
{this.enable = true,
|
||||
this.systemProxy = true,
|
||||
this.ipv6 = false,
|
||||
this.allowBypass = true});
|
||||
|
||||
factory _$VpnPropsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$VpnPropsImplFromJson(json);
|
||||
@@ -1044,11 +1079,14 @@ class _$VpnPropsImpl implements _VpnProps {
|
||||
final bool systemProxy;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool ipv6;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool allowBypass;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VpnProps(enable: $enable, systemProxy: $systemProxy, allowBypass: $allowBypass)';
|
||||
return 'VpnProps(enable: $enable, systemProxy: $systemProxy, ipv6: $ipv6, allowBypass: $allowBypass)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1059,6 +1097,7 @@ class _$VpnPropsImpl implements _VpnProps {
|
||||
(identical(other.enable, enable) || other.enable == enable) &&
|
||||
(identical(other.systemProxy, systemProxy) ||
|
||||
other.systemProxy == systemProxy) &&
|
||||
(identical(other.ipv6, ipv6) || other.ipv6 == ipv6) &&
|
||||
(identical(other.allowBypass, allowBypass) ||
|
||||
other.allowBypass == allowBypass));
|
||||
}
|
||||
@@ -1066,7 +1105,7 @@ class _$VpnPropsImpl implements _VpnProps {
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, enable, systemProxy, allowBypass);
|
||||
Object.hash(runtimeType, enable, systemProxy, ipv6, allowBypass);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@@ -1086,6 +1125,7 @@ abstract class _VpnProps implements VpnProps {
|
||||
const factory _VpnProps(
|
||||
{final bool enable,
|
||||
final bool systemProxy,
|
||||
final bool ipv6,
|
||||
final bool allowBypass}) = _$VpnPropsImpl;
|
||||
|
||||
factory _VpnProps.fromJson(Map<String, dynamic> json) =
|
||||
@@ -1096,6 +1136,8 @@ abstract class _VpnProps implements VpnProps {
|
||||
@override
|
||||
bool get systemProxy;
|
||||
@override
|
||||
bool get ipv6;
|
||||
@override
|
||||
bool get allowBypass;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
|
||||
@@ -176,6 +176,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
|
||||
allowBypass: json['allowBypass'] as bool,
|
||||
systemProxy: json['systemProxy'] as bool,
|
||||
mixedPort: (json['mixedPort'] as num).toInt(),
|
||||
ipv6: json['ipv6'] as bool,
|
||||
onlyProxy: json['onlyProxy'] as bool,
|
||||
);
|
||||
|
||||
@@ -187,6 +188,7 @@ Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
|
||||
'allowBypass': instance.allowBypass,
|
||||
'systemProxy': instance.systemProxy,
|
||||
'mixedPort': instance.mixedPort,
|
||||
'ipv6': instance.ipv6,
|
||||
'onlyProxy': instance.onlyProxy,
|
||||
};
|
||||
|
||||
@@ -224,7 +226,8 @@ Map<String, dynamic> _$$WindowPropsImplToJson(_$WindowPropsImpl instance) =>
|
||||
_$VpnPropsImpl _$$VpnPropsImplFromJson(Map<String, dynamic> json) =>
|
||||
_$VpnPropsImpl(
|
||||
enable: json['enable'] as bool? ?? true,
|
||||
systemProxy: json['systemProxy'] as bool? ?? false,
|
||||
systemProxy: json['systemProxy'] as bool? ?? true,
|
||||
ipv6: json['ipv6'] as bool? ?? false,
|
||||
allowBypass: json['allowBypass'] as bool? ?? true,
|
||||
);
|
||||
|
||||
@@ -232,6 +235,7 @@ Map<String, dynamic> _$$VpnPropsImplToJson(_$VpnPropsImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'enable': instance.enable,
|
||||
'systemProxy': instance.systemProxy,
|
||||
'ipv6': instance.ipv6,
|
||||
'allowBypass': instance.allowBypass,
|
||||
};
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class GlobalState {
|
||||
startTime = clashCore.getRunTime();
|
||||
}
|
||||
|
||||
handleStop() async {
|
||||
Future handleStop() async {
|
||||
clashCore.stop();
|
||||
if (Platform.isAndroid) {
|
||||
clashCore.stopTun();
|
||||
@@ -133,6 +133,7 @@ class GlobalState {
|
||||
CoreState(
|
||||
enable: config.vpnProps.enable,
|
||||
accessControl: config.isAccessControl ? config.accessControl : null,
|
||||
ipv6: config.vpnProps.ipv6,
|
||||
allowBypass: config.vpnProps.allowBypass,
|
||||
systemProxy: config.vpnProps.systemProxy,
|
||||
mixedPort: clashConfig.mixedPort,
|
||||
|
||||
@@ -149,7 +149,6 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
final Widget Function(T item)? subtitleBuilder;
|
||||
final Function(T item) onAdd;
|
||||
final Function(T item) onRemove;
|
||||
final bool isMap;
|
||||
|
||||
const UpdatePage({
|
||||
super.key,
|
||||
@@ -158,10 +157,37 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
required this.titleBuilder,
|
||||
required this.onRemove,
|
||||
required this.onAdd,
|
||||
this.isMap = false,
|
||||
this.subtitleBuilder,
|
||||
});
|
||||
|
||||
bool get isMap => items is Iterable<MapEntry>;
|
||||
|
||||
_handleEdit(T item) async {
|
||||
if (isMap) {
|
||||
item as MapEntry<String, String>;
|
||||
final value = await globalState.showCommonDialog<T>(
|
||||
child: AddDialog(
|
||||
defaultKey: item.key,
|
||||
defaultValue: item.value,
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
if (value == null) return;
|
||||
onAdd(value);
|
||||
} else {
|
||||
item as String;
|
||||
final value = await globalState.showCommonDialog<T>(
|
||||
child: AddDialog(
|
||||
defaultKey: null,
|
||||
defaultValue: item,
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
if (value == null) return;
|
||||
onAdd(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatLayout(
|
||||
@@ -170,7 +196,8 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
onPressed: () async {
|
||||
final value = await globalState.showCommonDialog<T>(
|
||||
child: AddDialog(
|
||||
isMap: isMap,
|
||||
defaultKey: isMap ? "" : null,
|
||||
defaultValue: "",
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
@@ -202,7 +229,9 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
_handleEdit(e);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -213,12 +242,14 @@ class UpdatePage<T> extends StatelessWidget {
|
||||
|
||||
class AddDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final bool isMap;
|
||||
final String? defaultKey;
|
||||
final String defaultValue;
|
||||
|
||||
const AddDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.isMap,
|
||||
this.defaultKey,
|
||||
required this.defaultValue,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -233,13 +264,19 @@ class _AddDialogState extends State<AddDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
keyController = TextEditingController();
|
||||
valueController = TextEditingController();
|
||||
keyController = TextEditingController(
|
||||
text: widget.defaultKey,
|
||||
);
|
||||
valueController = TextEditingController(
|
||||
text: widget.defaultValue,
|
||||
);
|
||||
}
|
||||
|
||||
bool get hasKey => widget.defaultKey != null;
|
||||
|
||||
_submit() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
if (widget.isMap) {
|
||||
if (hasKey) {
|
||||
Navigator.of(context).pop<MapEntry<String, String>>(
|
||||
MapEntry(
|
||||
keyController.text,
|
||||
@@ -264,7 +301,7 @@ class _AddDialogState extends State<AddDialog> {
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
if (widget.isMap)
|
||||
if (hasKey)
|
||||
TextFormField(
|
||||
maxLines: 2,
|
||||
minLines: 1,
|
||||
|
||||
Reference in New Issue
Block a user