Remake desktop

Optimize change proxy

Optimize network check

Fix fallback issues

Optimize lots of details
This commit is contained in:
chen08209
2024-12-03 21:47:12 +08:00
parent 4b32a096dd
commit ece8a48181
96 changed files with 5869 additions and 2378 deletions

View File

@@ -1,8 +1,10 @@
import 'dart:async';
import 'package:animations/animations.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
@@ -245,8 +247,10 @@ class ApplicationState extends State<Application> {
@override
Future<void> dispose() async {
linkManager.destroy();
await globalState.appController.savePreferences();
super.dispose();
_cancelTimer();
await clashService?.destroy();
await globalState.appController.savePreferences();
await globalState.appController.handleExit();
super.dispose();
}
}

View File

@@ -1,3 +1,4 @@
export 'core.dart';
export 'lib.dart';
export 'message.dart';
export 'service.dart';
export 'message.dart';

View File

@@ -1,42 +1,26 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'generated/clash_ffi.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
class ClashCore {
static ClashCore? _instance;
static final receiver = ReceivePort();
late final ClashFFI clashFFI;
late final DynamicLibrary lib;
DynamicLibrary _getClashLib() {
if (Platform.isWindows) {
return DynamicLibrary.open("libclash.dll");
}
if (Platform.isMacOS) {
return DynamicLibrary.open("libclash.dylib");
}
if (Platform.isAndroid || Platform.isLinux) {
return DynamicLibrary.open("libclash.so");
}
throw "Platform is not supported";
}
late ClashInterface clashInterface;
ClashCore._internal() {
lib = _getClashLib();
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
if (Platform.isAndroid) {
clashInterface = clashLib!;
} else {
clashInterface = clashService!;
}
}
factory ClashCore() {
@@ -44,67 +28,62 @@ class ClashCore {
return _instance!;
}
bool init(String homeDir) {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
}
shutdown() {
clashFFI.shutdownClash();
lib.close();
}
bool get isInit => clashFFI.getIsInit() == 1;
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
Future<void> _initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];
try {
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
final isExists = await geoFile.exists();
if (isExists) {
continue;
}
final data = await rootBundle.load('assets/data/$geoFileName');
List<int> bytes = data.buffer.asUint8List();
await geoFile.writeAsBytes(bytes, flush: true);
}
});
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.validateConfig(
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(dataChar);
return completer.future;
} catch (e) {
exit(0);
}
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(updateConfigParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.updateConfig(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
Future<bool> init({
required ClashConfig clashConfig,
required Config config,
}) async {
await _initGeo();
final homeDirPath = await appPath.getHomeDirPath();
return await clashInterface.init(homeDirPath);
}
initMessage() {
clashFFI.initMessage(
receiver.sendPort.nativePort,
);
shutdown() async {
await clashInterface.shutdown();
}
Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(proxiesRaw);
FutureOr<bool> get isInit => clashInterface.isInit;
FutureOr<String> validateConfig(String data) {
return clashInterface.validateConfig(data);
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await clashInterface.updateConfig(updateConfigParams);
}
Future<List<Group>> getProxiesGroups() async {
final proxiesRawString = await clashInterface.getProxies();
return Isolate.run<List<Group>>(() {
if (proxiesRawString.isEmpty) return [];
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
@@ -134,256 +113,112 @@ class ClashCore {
});
}
Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProvidersRaw);
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
});
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
return await clashInterface.changeProxy(changeProxyParams);
}
ExternalProvider? getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
if (externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
Future<List<Connection>> getConnections() async {
final res = await clashInterface.getConnections();
final connectionsData = json.decode(res) as Map;
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
closeConnection(String id) {
clashInterface.closeConnection(id);
}
closeConnections() {
clashInterface.closeConnections();
}
Future<List<ExternalProvider>> getExternalProviders() async {
final externalProvidersRawString =
await clashInterface.getExternalProviders();
return Isolate.run<List<ExternalProvider>>(
() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
},
);
}
Future<ExternalProvider?> getExternalProvider(
String externalProviderName) async {
final externalProvidersRawString =
await clashInterface.getExternalProvider(externalProviderName);
if (externalProvidersRawString == null) {
return null;
}
if (externalProvidersRawString.isEmpty) {
return null;
}
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
}
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
clashFFI.updateGeoData(
geoTypeChar,
geoNameChar,
receiver.sendPort.nativePort,
);
malloc.free(geoTypeChar);
malloc.free(geoNameChar);
return completer.future;
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
}
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
return clashInterface.sideLoadExternalProvider(
providerName: providerName, data: data);
}
Future<String> updateExternalProvider({
required String providerName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
return completer.future;
}) async {
return clashInterface.updateExternalProvider(providerName);
}
changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.changeProxy(paramsChar);
malloc.free(paramsChar);
startListener() async {
await clashInterface.startListener();
}
start() {
clashFFI.start();
stopListener() async {
await clashInterface.stopListener();
}
stop() {
clashFFI.stop();
Future<Delay> getDelay(String proxyName) async {
final data = await clashInterface.asyncTestDelay(proxyName);
return Delay.fromJson(json.decode(data));
}
Future<Delay> getDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
final completer = Completer<Delay>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(Delay.fromJson(json.decode(message)));
receiver.close();
}
});
final delayParamsChar =
json.encode(delayParams).toNativeUtf8().cast<Char>();
clashFFI.asyncTestDelay(
delayParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(delayParamsChar);
return completer.future;
Future<Traffic> getTraffic(bool value) async {
final trafficString = await clashInterface.getTraffic(value);
return Traffic.fromMap(json.decode(trafficString));
}
clearEffect(String profileId) {
final profileIdChar = profileId.toNativeUtf8().cast<Char>();
clashFFI.clearEffect(profileIdChar);
malloc.free(profileIdChar);
Future<Traffic> getTotalTraffic(bool value) async {
final totalTrafficString = await clashInterface.getTotalTraffic(value);
return Traffic.fromMap(json.decode(totalTrafficString));
}
VersionInfo getVersionInfo() {
final versionInfoRaw = clashFFI.getVersionInfo();
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(versionInfoRaw);
return VersionInfo.fromJson(versionInfo);
resetTraffic() {
clashInterface.resetTraffic();
}
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar);
malloc.free(stateChar);
}
String getCurrentProfileName() {
final currentProfileRaw = clashFFI.getCurrentProfileName();
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileRaw);
return currentProfile;
}
AndroidVpnOptions getAndroidVpnOptions() {
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(vpnOptionsRaw);
return AndroidVpnOptions.fromJson(vpnOptions);
}
Traffic getTraffic() {
final trafficRaw = clashFFI.getTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(trafficRaw);
return Traffic.fromMap(trafficMap);
}
Traffic getTotalTraffic() {
final trafficRaw = clashFFI.getTotalTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(trafficRaw);
return Traffic.fromMap(trafficMap);
}
void resetTraffic() {
clashFFI.resetTraffic();
}
void startLog() {
clashFFI.startLog();
startLog() {
clashInterface.startLog();
}
stopLog() {
clashFFI.stopLog();
}
startTun(int fd, int port) {
if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
}
updateDns(String dns) {
if (!Platform.isAndroid) return;
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
clashInterface.stopLog();
}
requestGc() {
clashFFI.forceGc();
}
void stopTun() {
clashFFI.stopTun();
}
void setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
void setFdMap(int fd) {
clashFFI.setFdMap(fd);
}
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
List<Connection> getConnections() {
final connectionsDataRaw = clashFFI.getConnections();
final connectionsData =
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
clashFFI.freeCString(connectionsDataRaw);
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
closeConnection(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.closeConnection(idChar);
malloc.free(idChar);
}
closeConnections() {
clashFFI.closeConnections();
clashInterface.forceGc();
}
}

View File

@@ -2362,21 +2362,46 @@ class ClashFFI {
late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void start() {
return _start();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
) {
return _initNativeApiBridge(
api,
);
}
late final _startPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('start');
late final _start = _startPtr.asFunction<void Function()>();
late final _initNativeApiBridgePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'initNativeApiBridge');
late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void stop() {
return _stop();
void initMessage(
int port,
) {
return _initMessage(
port,
);
}
late final _stopPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stop');
late final _stop = _stopPtr.asFunction<void Function()>();
late final _initMessagePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'initMessage');
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
void freeCString(
ffi.Pointer<ffi.Char> s,
) {
return _freeCString(
s,
);
}
late final _freeCStringPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'freeCString');
late final _freeCString =
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
int initClash(
ffi.Pointer<ffi.Char> homeDirStr,
@@ -2392,6 +2417,22 @@ class ClashFFI {
late final _initClash =
_initClashPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
void startListener() {
return _startListener();
}
late final _startListenerPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('startListener');
late final _startListener = _startListenerPtr.asFunction<void Function()>();
void stopListener() {
return _stopListener();
}
late final _stopListenerPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
int getIsInit() {
return _getIsInit();
}
@@ -2400,14 +2441,6 @@ class ClashFFI {
_lookup<ffi.NativeFunction<GoUint8 Function()>>('getIsInit');
late final _getIsInit = _getIsInitPtr.asFunction<int Function()>();
int restartClash() {
return _restartClash();
}
late final _restartClashPtr =
_lookup<ffi.NativeFunction<GoUint8 Function()>>('restartClash');
late final _restartClash = _restartClashPtr.asFunction<int Function()>();
int shutdownClash() {
return _shutdownClash();
}
@@ -2458,20 +2491,6 @@ class ClashFFI {
late final _updateConfig =
_updateConfigPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void clearEffect(
ffi.Pointer<ffi.Char> s,
) {
return _clearEffect(
s,
);
}
late final _clearEffectPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'clearEffect');
late final _clearEffect =
_clearEffectPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getProxies() {
return _getProxies();
}
@@ -2484,37 +2503,48 @@ class ClashFFI {
void changeProxy(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _changeProxy(
s,
port,
);
}
late final _changeProxyPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'changeProxy');
late final _changeProxyPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('changeProxy');
late final _changeProxy =
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getTraffic() {
return _getTraffic();
ffi.Pointer<ffi.Char> getTraffic(
int port,
) {
return _getTraffic(
port,
);
}
late final _getTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'getTraffic');
late final _getTraffic =
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
ffi.Pointer<ffi.Char> getTotalTraffic() {
return _getTotalTraffic();
ffi.Pointer<ffi.Char> getTotalTraffic(
int port,
) {
return _getTotalTraffic(
port,
);
}
late final _getTotalTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'getTotalTraffic');
late final _getTotalTraffic =
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
void resetTraffic() {
return _resetTraffic();
@@ -2541,16 +2571,6 @@ class ClashFFI {
late final _asyncTestDelay = _asyncTestDelayPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getVersionInfo() {
return _getVersionInfo();
}
late final _getVersionInfoPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getVersionInfo');
late final _getVersionInfo =
_getVersionInfoPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getConnections() {
return _getConnections();
}
@@ -2595,10 +2615,10 @@ class ClashFFI {
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getExternalProvider(
ffi.Pointer<ffi.Char> name,
ffi.Pointer<ffi.Char> externalProviderNameChar,
) {
return _getExternalProvider(
name,
externalProviderNameChar,
);
}
@@ -2610,13 +2630,13 @@ class ClashFFI {
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void updateGeoData(
ffi.Pointer<ffi.Char> geoType,
ffi.Pointer<ffi.Char> geoName,
ffi.Pointer<ffi.Char> geoTypeChar,
ffi.Pointer<ffi.Char> geoNameChar,
int port,
) {
return _updateGeoData(
geoType,
geoName,
geoTypeChar,
geoNameChar,
port,
);
}
@@ -2629,11 +2649,11 @@ class ClashFFI {
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void updateExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> providerNameChar,
int port,
) {
return _updateExternalProvider(
providerName,
providerNameChar,
port,
);
}
@@ -2646,13 +2666,13 @@ class ClashFFI {
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void sideLoadExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> data,
ffi.Pointer<ffi.Char> providerNameChar,
ffi.Pointer<ffi.Char> dataChar,
int port,
) {
return _sideLoadExternalProvider(
providerName,
data,
providerNameChar,
dataChar,
port,
);
}
@@ -2665,47 +2685,6 @@ class ClashFFI {
_sideLoadExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
) {
return _initNativeApiBridge(
api,
);
}
late final _initNativeApiBridgePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'initNativeApiBridge');
late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void initMessage(
int port,
) {
return _initMessage(
port,
);
}
late final _initMessagePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'initMessage');
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
void freeCString(
ffi.Pointer<ffi.Char> s,
) {
return _freeCString(
s,
);
}
late final _freeCStringPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'freeCString');
late final _freeCString =
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void startLog() {
return _startLog();
}
@@ -2722,6 +2701,51 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
void startTUN(
int fd,
int port,
) {
return _startTUN(
fd,
port,
);
}
late final _startTUNPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
'startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime();
}
late final _getRunTimePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getRunTime');
late final _getRunTime =
_getRunTimePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void stopTun() {
return _stopTun();
}
late final _stopTunPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap(
int fd,
) {
return _setFdMap(
fd,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
void setProcessMap(
ffi.Pointer<ffi.Char> s,
) {
@@ -2769,51 +2793,6 @@ class ClashFFI {
'setState');
late final _setState =
_setStatePtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void startTUN(
int fd,
int port,
) {
return _startTUN(
fd,
port,
);
}
late final _startTUNPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
'startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime();
}
late final _getRunTimePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getRunTime');
late final _getRunTime =
_getRunTimePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void stopTun() {
return _stopTun();
}
late final _stopTunPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap(
int fd,
) {
return _setFdMap(
fd,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
}
final class __mbstate_t extends ffi.Union {

59
lib/clash/interface.dart Normal file
View File

@@ -0,0 +1,59 @@
import 'dart:async';
import 'package:fl_clash/models/models.dart';
mixin ClashInterface {
FutureOr<bool> init(String homeDir);
FutureOr<void> shutdown();
FutureOr<bool> get isInit;
forceGc();
FutureOr<String> validateConfig(String data);
Future<String> asyncTestDelay(String proxyName);
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
FutureOr<String> getProxies();
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams);
Future<bool> startListener();
Future<bool> stopListener();
FutureOr<String> getExternalProviders();
FutureOr<String>? getExternalProvider(String externalProviderName);
Future<String> updateGeoData({
required String geoType,
required String geoName,
});
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
});
Future<String> updateExternalProvider(String providerName);
FutureOr<String> getTraffic(bool value);
FutureOr<String> getTotalTraffic(bool value);
resetTraffic();
startLog();
stopLog();
FutureOr<String> getConnections();
FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections();
}

367
lib/clash/lib.dart Normal file
View File

@@ -0,0 +1,367 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/models/models.dart';
import 'generated/clash_ffi.dart';
import 'interface.dart';
class ClashLib with ClashInterface {
static ClashLib? _instance;
final receiver = ReceivePort();
late final ClashFFI clashFFI;
late final DynamicLibrary lib;
ClashLib._internal() {
lib = DynamicLibrary.open("libclash.so");
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
}
factory ClashLib() {
_instance ??= ClashLib._internal();
return _instance!;
}
initMessage() {
clashFFI.initMessage(
receiver.sendPort.nativePort,
);
}
@override
bool init(String homeDir) {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
}
@override
shutdown() async {
clashFFI.shutdownClash();
lib.close();
}
@override
bool get isInit => clashFFI.getIsInit() == 1;
@override
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.validateConfig(
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(dataChar);
return completer.future;
}
@override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(updateConfigParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.updateConfig(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
}
@override
String getProxies() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(proxiesRaw);
return proxiesRawString;
}
@override
String getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProvidersRaw);
return externalProvidersRawString;
}
@override
String getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
return externalProviderRawString;
}
@override
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
clashFFI.updateGeoData(
geoTypeChar,
geoNameChar,
receiver.sendPort.nativePort,
);
malloc.free(geoTypeChar);
malloc.free(geoNameChar);
return completer.future;
}
@override
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
}
@override
Future<String> updateExternalProvider(String providerName) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
return completer.future;
}
@override
Future<String> changeProxy(ChangeProxyParams changeProxyParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.changeProxy(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
}
@override
String getConnections() {
final connectionsDataRaw = clashFFI.getConnections();
final connectionsString = connectionsDataRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(connectionsDataRaw);
return connectionsString;
}
@override
closeConnection(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.closeConnection(idChar);
malloc.free(idChar);
return true;
}
@override
closeConnections() {
clashFFI.closeConnections();
return true;
}
@override
startListener() async {
clashFFI.startListener();
return true;
}
@override
stopListener() async {
clashFFI.stopListener();
return true;
}
@override
Future<String> asyncTestDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final delayParamsChar =
json.encode(delayParams).toNativeUtf8().cast<Char>();
clashFFI.asyncTestDelay(
delayParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(delayParamsChar);
return completer.future;
}
@override
String getTraffic(bool value) {
final trafficRaw = clashFFI.getTraffic(value ? 1 : 0);
final trafficString = trafficRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(trafficRaw);
return trafficString;
}
@override
String getTotalTraffic(bool value) {
final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0);
clashFFI.freeCString(trafficRaw);
return trafficRaw.cast<Utf8>().toDartString();
}
@override
void resetTraffic() {
clashFFI.resetTraffic();
}
@override
void startLog() {
clashFFI.startLog();
}
@override
stopLog() {
clashFFI.stopLog();
}
@override
forceGc() {
clashFFI.forceGc();
}
/// Android
startTun(int fd, int port) {
if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
}
stopTun() {
clashFFI.stopTun();
}
updateDns(String dns) {
if (!Platform.isAndroid) return;
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
}
setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar);
malloc.free(stateChar);
}
String getCurrentProfileName() {
final currentProfileRaw = clashFFI.getCurrentProfileName();
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileRaw);
return currentProfile;
}
AndroidVpnOptions getAndroidVpnOptions() {
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(vpnOptionsRaw);
return AndroidVpnOptions.fromJson(vpnOptions);
}
setFdMap(int fd) {
clashFFI.setFdMap(fd);
}
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
}
final clashLib = Platform.isAndroid ? ClashLib() : null;

View File

@@ -1,42 +1,40 @@
import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart';
import 'core.dart';
class ClashMessage {
StreamSubscription? subscription;
final controller = StreamController();
ClashMessage._() {
if (subscription != null) {
subscription!.cancel();
subscription = null;
}
subscription = ClashCore.receiver.listen((message) {
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
listener.onLog(Log.fromJson(m.data));
break;
case AppMessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break;
clashLib?.receiver.listen(controller.add);
controller.stream.listen(
(message) {
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
listener.onLog(Log.fromJson(m.data));
break;
case AppMessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break;
}
}
}
});
},
);
}
static final ClashMessage instance = ClashMessage._();

View File

@@ -1,54 +1,414 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/core.dart';
import 'core.dart';
class ClashService with ClashInterface {
static ClashService? _instance;
class ClashService {
Future<void> initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];
try {
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
final isExists = await geoFile.exists();
if (isExists) {
continue;
}
final data = await rootBundle.load('assets/data/$geoFileName');
List<int> bytes = data.buffer.asUint8List();
await geoFile.writeAsBytes(bytes, flush: true);
}
} catch (e) {
debugPrint("$e");
exit(0);
Completer<ServerSocket> serverCompleter = Completer();
Completer<Socket> socketCompleter = Completer();
Map<String, Completer> callbackCompleterMap = {};
Process? process;
factory ClashService() {
_instance ??= ClashService._internal();
return _instance!;
}
ClashService._internal() {
_createServer();
startCore();
}
_createServer() async {
final address = !Platform.isWindows
? InternetAddress(
unixSocketPath,
type: InternetAddressType.unix,
)
: InternetAddress(
localhost,
type: InternetAddressType.IPv4,
);
await _deleteSocketFile();
final server = await ServerSocket.bind(
address,
0,
shared: true,
);
serverCompleter.complete(server);
await for (final socket in server) {
await _destroySocket();
socketCompleter.complete(socket);
socket
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
_handleAction(
Action.fromJson(
json.decode(data.trim()),
),
);
},
);
}
}
Future<bool> init({
required ClashConfig clashConfig,
required Config config,
startCore() async {
if (process != null) {
await shutdown();
}
final serverSocket = await serverCompleter.future;
final arg = Platform.isWindows
? "${serverSocket.port}"
: serverSocket.address.address;
bool isSuccess = false;
if (Platform.isWindows && await system.checkIsAdmin()) {
isSuccess = await request.startCoreByHelper(arg);
}
if (isSuccess) {
return;
}
process = await Process.start(
appPath.corePath,
[
arg,
],
);
process!.stdout.listen((_) {});
}
_deleteSocketFile() async {
if (!Platform.isWindows) {
final file = File(unixSocketPath);
if (await file.exists()) {
await file.delete();
}
}
}
_destroySocket() async {
if (socketCompleter.isCompleted) {
final lastSocket = await socketCompleter.future;
await lastSocket.close();
socketCompleter = Completer();
}
}
_handleAction(Action action) {
final completer = callbackCompleterMap[action.id];
switch (action.method) {
case ActionMethod.initClash:
case ActionMethod.shutdown:
case ActionMethod.getIsInit:
case ActionMethod.startListener:
case ActionMethod.resetTraffic:
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
completer?.complete(action.data as bool);
return;
case ActionMethod.changeProxy:
case ActionMethod.getProxies:
case ActionMethod.getTraffic:
case ActionMethod.getTotalTraffic:
case ActionMethod.asyncTestDelay:
case ActionMethod.getConnections:
case ActionMethod.getExternalProviders:
case ActionMethod.getExternalProvider:
case ActionMethod.validateConfig:
case ActionMethod.updateConfig:
case ActionMethod.updateGeoData:
case ActionMethod.updateExternalProvider:
case ActionMethod.sideLoadExternalProvider:
completer?.complete(action.data as String);
return;
case ActionMethod.message:
clashMessage.controller.add(action.data as String);
return;
case ActionMethod.forceGc:
case ActionMethod.startLog:
case ActionMethod.stopLog:
default:
return;
}
}
Future<T> _invoke<T>({
required ActionMethod method,
dynamic data,
Duration? timeout,
FutureOr<T> Function()? onTimeout,
}) async {
await initGeo();
final homeDirPath = await appPath.getHomeDirPath();
final isInit = clashCore.init(homeDirPath);
return isInit;
final id = "${method.name}#${other.id}";
final socket = await socketCompleter.future;
callbackCompleterMap[id] = Completer<T>();
socket.writeln(
json.encode(
Action(
id: id,
method: method,
data: data,
),
),
);
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
timeout: timeout,
onLast: () {
callbackCompleterMap.remove(id);
},
onTimeout: onTimeout,
functionName: id,
);
}
_prueInvoke({
required ActionMethod method,
dynamic data,
}) async {
final id = "${method.name}#${other.id}";
final socket = await socketCompleter.future;
socket.writeln(
json.encode(
Action(
id: id,
method: method,
data: data,
),
),
);
}
@override
Future<bool> init(String homeDir) {
return _invoke<bool>(
method: ActionMethod.initClash,
data: homeDir,
);
}
@override
shutdown() async {
await _invoke<bool>(
method: ActionMethod.shutdown,
);
if (Platform.isWindows) {
await request.stopCoreByHelper();
}
await _destroySocket();
process?.kill();
process = null;
}
@override
Future<bool> get isInit {
return _invoke<bool>(
method: ActionMethod.getIsInit,
);
}
@override
forceGc() {
_prueInvoke(method: ActionMethod.forceGc);
}
@override
FutureOr<String> validateConfig(String data) {
return _invoke<String>(
method: ActionMethod.validateConfig,
data: data,
);
}
@override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await _invoke<String>(
method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams),
timeout: const Duration(seconds: 20),
);
}
@override
Future<String> getProxies() {
return _invoke<String>(
method: ActionMethod.getProxies,
);
}
@override
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
return _invoke<String>(
method: ActionMethod.changeProxy,
data: json.encode(changeProxyParams),
);
}
@override
FutureOr<String> getExternalProviders() {
return _invoke<String>(
method: ActionMethod.getExternalProviders,
);
}
@override
FutureOr<String> getExternalProvider(String externalProviderName) {
return _invoke<String>(
method: ActionMethod.getExternalProvider,
data: externalProviderName,
);
}
@override
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
return _invoke<String>(
method: ActionMethod.updateGeoData,
data: json.encode(
{
"geoType": geoType,
"geoName": geoName,
},
),
);
}
@override
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
return _invoke<String>(
method: ActionMethod.sideLoadExternalProvider,
data: json.encode({
"providerName": providerName,
"data": data,
}),
);
}
@override
Future<String> updateExternalProvider(String providerName) {
return _invoke<String>(
method: ActionMethod.updateExternalProvider,
data: providerName,
);
}
@override
FutureOr<String> getConnections() {
return _invoke<String>(
method: ActionMethod.getConnections,
);
}
@override
Future<bool> closeConnections() {
return _invoke<bool>(
method: ActionMethod.closeConnections,
);
}
@override
Future<bool> closeConnection(String id) {
return _invoke<bool>(
method: ActionMethod.closeConnection,
data: id,
);
}
@override
FutureOr<String> getTotalTraffic(bool value) {
return _invoke<String>(
method: ActionMethod.getTotalTraffic,
data: value,
);
}
@override
FutureOr<String> getTraffic(bool value) {
return _invoke<String>(
method: ActionMethod.getTraffic,
data: value,
);
}
@override
resetTraffic() {
_prueInvoke(method: ActionMethod.resetTraffic);
}
@override
startLog() {
_prueInvoke(method: ActionMethod.startLog);
}
@override
stopLog() {
_prueInvoke(method: ActionMethod.stopLog);
}
@override
Future<bool> startListener() {
return _invoke<bool>(
method: ActionMethod.startListener,
);
}
@override
stopListener() {
return _invoke<bool>(
method: ActionMethod.stopListener,
);
}
@override
Future<String> asyncTestDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
return _invoke<String>(
method: ActionMethod.asyncTestDelay,
data: json.encode(delayParams),
timeout: Duration(
milliseconds: 6000,
),
onTimeout: () {
return json.encode(
Delay(
name: proxyName,
value: -1,
),
);
},
);
}
destroy() async {
final server = await serverCompleter.future;
await server.close();
await _deleteSocketFile();
}
}
final clashService = ClashService();
final clashService = system.isDesktop ? ClashService() : null;

View File

@@ -1,33 +1,36 @@
export 'path.dart';
export 'request.dart';
export 'preferences.dart';
export 'constant.dart';
export 'proxy.dart';
export 'other.dart';
export 'num.dart';
export 'navigation.dart';
export 'window.dart';
export 'system.dart';
export 'picker.dart';
export 'android.dart';
export 'launch.dart';
export 'protocol.dart';
export 'datetime.dart';
export 'context.dart';
export 'link.dart';
export 'text.dart';
export 'color.dart';
export 'list.dart';
export 'string.dart';
export 'app_localizations.dart';
export 'color.dart';
export 'constant.dart';
export 'context.dart';
export 'datetime.dart';
export 'function.dart';
export 'package.dart';
export 'measure.dart';
export 'windows.dart';
export 'iterable.dart';
export 'scroll.dart';
export 'icons.dart';
export 'future.dart';
export 'http.dart';
export 'icons.dart';
export 'iterable.dart';
export 'keyboard.dart';
export 'launch.dart';
export 'link.dart';
export 'list.dart';
export 'lock.dart';
export 'measure.dart';
export 'navigation.dart';
export 'navigator.dart';
export 'network.dart';
export 'navigator.dart';
export 'num.dart';
export 'other.dart';
export 'package.dart';
export 'path.dart';
export 'picker.dart';
export 'preferences.dart';
export 'protocol.dart';
export 'proxy.dart';
export 'request.dart';
export 'scroll.dart';
export 'string.dart';
export 'system.dart';
export 'text.dart';
export 'tray.dart';
export 'window.dart';
export 'windows.dart';

View File

@@ -1,15 +1,20 @@
import 'dart:io';
import 'dart:math';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'system.dart';
const appName = "FlClash";
const appHelperService = "FlClashHelperService";
const coreName = "clash.meta";
const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
const helperTag = "2024125";
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
@@ -21,7 +26,7 @@ const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop
? !Platform.isMacOS
? 40
: 26
: 28
: 0;
const GeoXMap defaultGeoXMap = {
"mmdb":

View File

@@ -24,8 +24,8 @@ class DAVClient {
},
);
client.setConnectTimeout(8000);
client.setSendTimeout(8000);
client.setReceiveTimeout(8000);
client.setSendTimeout(60000);
client.setReceiveTimeout(60000);
pingCompleter.complete(_ping());
}

42
lib/common/future.dart Normal file
View File

@@ -0,0 +1,42 @@
import 'dart:async';
import 'dart:ui';
extension CompleterExt<T> on Completer<T> {
safeFuture({
Duration? timeout,
VoidCallback? onLast,
FutureOr<T> Function()? onTimeout,
required String functionName,
}) {
final realTimeout = timeout ?? const Duration(seconds: 6);
Timer(realTimeout + Duration(milliseconds: 1000), () {
if (onLast != null) {
onLast();
}
});
return future.withTimeout(
timeout: realTimeout,
functionName: functionName,
onTimeout: onTimeout,
);
}
}
extension FutureExt<T> on Future<T> {
Future<T> withTimeout({
required Duration timeout,
required String functionName,
FutureOr<T> Function()? onTimeout,
}) {
return this.timeout(
timeout,
onTimeout: () async {
if (onTimeout != null) {
return onTimeout();
} else {
throw TimeoutException('$functionName timeout');
}
},
);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/cupertino.dart';
import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides {
@override
@@ -10,6 +11,9 @@ class FlClashHttpOverrides extends HttpOverrides {
final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = (url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
debugPrint("find $url");
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;

View File

@@ -1,11 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/models/models.dart' hide Process;
import 'package:fl_clash/models/models.dart';
import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart';
import 'system.dart';
import 'windows.dart';
class AutoLaunch {
static AutoLaunch? _instance;
@@ -26,60 +26,16 @@ class AutoLaunch {
return await launchAtStartup.isEnabled();
}
Future<bool> get windowsIsEnable async {
final res = await Process.run(
'schtasks',
['/Query', '/TN', appName, '/V', "/FO", "LIST"],
runInShell: true,
);
return res.stdout.toString().contains(Platform.resolvedExecutable);
}
Future<bool> enable() async {
if (Platform.isWindows) {
await windowsDisable();
}
return await launchAtStartup.enable();
}
windowsDisable() async {
final res = await Process.run(
'schtasks',
[
'/Delete',
'/TN',
appName,
'/F',
],
runInShell: true,
);
return res.exitCode == 0;
}
Future<bool> windowsEnable() async {
await disable();
return await windows?.registerTask(appName) ?? false;
}
Future<bool> disable() async {
return await launchAtStartup.disable();
}
updateStatus(AutoLaunchState state) async {
final isAdminAutoLaunch = state.isAdminAutoLaunch;
final isAutoLaunch = state.isAutoLaunch;
if (Platform.isWindows && isAdminAutoLaunch) {
if (await windowsIsEnable == isAutoLaunch) return;
if (isAutoLaunch) {
final isEnable = await windowsEnable();
if (!isEnable) {
enable();
}
} else {
windowsDisable();
}
return;
}
if (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) {
enable();

30
lib/common/lock.dart Normal file
View File

@@ -0,0 +1,30 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
class SingleInstanceLock {
static SingleInstanceLock? _instance;
RandomAccessFile? _accessFile;
SingleInstanceLock._internal();
factory SingleInstanceLock() {
_instance ??= SingleInstanceLock._internal();
return _instance!;
}
Future<bool> acquire() async {
try {
final lockFilePath = await appPath.getLockFilePath();
final lockFile = File(lockFilePath);
await lockFile.create();
_accessFile = await lockFile.open(mode: FileMode.write);
await _accessFile?.lock();
return true;
} catch (_) {
return false;
}
}
}
final singleInstanceLock = SingleInstanceLock();

View File

@@ -7,9 +7,9 @@ import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img;
class Other {
Color? getDelayColor(int? delay) {
@@ -19,6 +19,14 @@ class Other {
return const Color(0xFFC57F0A);
}
String get id {
final timestamp = DateTime.now().microsecondsSinceEpoch;
final random = Random();
final randomStr =
String.fromCharCodes(List.generate(8, (_) => random.nextInt(26) + 97));
return "$timestamp$randomStr";
}
String getDateStringLast2(int value) {
var valueRaw = "0$value";
return valueRaw.substring(
@@ -104,7 +112,7 @@ class Other {
String getTrayIconPath({
required Brightness brightness,
}) {
if(Platform.isMacOS){
if (Platform.isMacOS) {
return "assets/images/icon_white.png";
}
final suffix = Platform.isWindows ? "ico" : "png";

View File

@@ -13,34 +13,17 @@ class AppPath {
Completer<Directory> tempDir = Completer();
late String appDirPath;
// Future<Directory> _createDesktopCacheDir() async {
// final dir = Directory(path);
// if (await dir.exists()) {
// await dir.create(recursive: true);
// }
// return dir;
// }
AppPath._internal() {
appDirPath = join(dirname(Platform.resolvedExecutable));
getApplicationSupportDirectory().then((value) {
dataDir.complete(value);
});
getTemporaryDirectory().then((value){
tempDir.complete(value);
getTemporaryDirectory().then((value) {
tempDir.complete(value);
});
getDownloadsDirectory().then((value) {
downloadDir.complete(value);
});
// if (Platform.isAndroid) {
// getApplicationSupportDirectory().then((value) {
// cacheDir.complete(value);
// });
// } else {
// _createDesktopCacheDir().then((value) {
// cacheDir.complete(value);
// });
// }
}
factory AppPath() {
@@ -48,6 +31,23 @@ class AppPath {
return _instance!;
}
String get executableExtension {
return Platform.isWindows ? ".exe" : "";
}
String get executableDirPath {
final currentExecutablePath = Platform.resolvedExecutable;
return dirname(currentExecutablePath);
}
String get corePath {
return join(executableDirPath, "FlClashCore$executableExtension");
}
String get helperPath {
return join(executableDirPath, "$appHelperService$executableExtension");
}
Future<String> getDownloadDirPath() async {
final directory = await downloadDir.future;
return directory.path;
@@ -58,6 +58,11 @@ class AppPath {
return directory.path;
}
Future<String> getLockFilePath() async {
final directory = await dataDir.future;
return join(directory.path, "FlClash.lock");
}
Future<String> getProfilesPath() async {
final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName);
@@ -69,6 +74,12 @@ class AppPath {
return join(directory, "$id.yaml");
}
Future<String?> getProvidersPath(String? id) async {
if (id == null) return null;
final directory = await getProfilesPath();
return join(directory, "providers", id);
}
Future<String> get tempPath async {
final directory = await tempDir.future;
return directory.path;

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
@@ -79,25 +81,96 @@ class Request {
for (final source in _ipInfoSources.entries) {
try {
final response = await _dio
.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
)
.timeout(
httpTimeoutDuration,
);
if (response.statusCode == 200 && response.data != null) {
return source.value(response.data!);
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.timeout(httpTimeoutDuration);
if (response.statusCode != 200 || response.data == null) {
continue;
}
return source.value(response.data!);
} catch (e) {
if (cancelToken?.isCancelled == true) {
if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled";
}
continue;
debugPrint("checkIp error ===> $e");
}
}
return null;
}
Future<bool> pingHelper() async {
try {
final response = await _dio
.get(
"http://$localhost:$helperPort/ping",
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
return (response.data as String) == helperTag;
} catch (_) {
return false;
}
}
Future<bool> startCoreByHelper(String arg) async {
try {
final response = await _dio
.post(
"http://$localhost:$helperPort/start",
data: json.encode({
"path": appPath.corePath,
"arg": arg,
}),
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
final data = response.data as String;
return data.isEmpty;
} catch (_) {
return false;
}
}
Future<bool> stopCoreByHelper() async {
try {
final response = await _dio
.post(
"http://$localhost:$helperPort/stop",
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
final data = response.data as String;
return data.isEmpty;
} catch (_) {
return false;
}
}
}
final request = Request();

View File

@@ -1,11 +1,13 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/input.dart';
import 'package:flutter/services.dart';
import 'window.dart';
class System {
static System? _instance;
@@ -19,12 +21,6 @@ class System {
bool get isDesktop =>
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
get isAdmin async {
if (!Platform.isWindows) return false;
final result = await Process.run('net', ['session'], runInShell: true);
return result.exitCode == 0;
}
Future<int> get version async {
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
return switch (Platform.operatingSystem) {
@@ -35,6 +31,73 @@ class System {
};
}
Future<bool> checkIsAdmin() async {
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
if (Platform.isWindows) {
final result = await windows?.checkService();
return result == WindowsHelperServiceStatus.running;
} else if (Platform.isMacOS) {
final result = await Process.run('stat', ['-f', '%Su:%Sg %Sp', corePath]);
final output = result.stdout.trim();
if (output.startsWith('root:admin') && output.contains('rws')) {
return true;
}
return false;
} else if (Platform.isLinux) {
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
final output = result.stdout.trim();
if (output.startsWith('root:') && output.contains('rwx')) {
return true;
}
return false;
}
return true;
}
Future<AuthorizeCode> authorizeCore() async {
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
final isAdmin = await checkIsAdmin();
if (isAdmin) {
return AuthorizeCode.none;
}
if (Platform.isWindows) {
final result = await windows?.registerService();
if (result == true) {
return AuthorizeCode.success;
}
return AuthorizeCode.error;
} else if (Platform.isMacOS) {
final shell = 'chown root:admin $corePath; chmod +sx $corePath';
final arguments = [
"-e",
'do shell script "$shell" with administrator privileges',
];
final result = await Process.run("osascript", arguments);
if (result.exitCode != 0) {
return AuthorizeCode.error;
}
return AuthorizeCode.success;
} else if (Platform.isLinux) {
final shell = Platform.environment['SHELL'] ?? 'bash';
final password = await globalState.showCommonDialog<String>(
child: InputDialog(
title: appLocalizations.pleaseInputAdminPassword,
value: '',
),
);
final arguments = [
"-c",
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"'
];
final result = await Process.run(shell, arguments);
if (result.exitCode != 0) {
return AuthorizeCode.error;
}
return AuthorizeCode.success;
}
return AuthorizeCode.error;
}
back() async {
await app?.moveTaskToBack();
await window?.hide();

193
lib/common/tray.dart Normal file
View File

@@ -0,0 +1,193 @@
import 'dart:io';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:tray_manager/tray_manager.dart';
import 'app_localizations.dart';
import 'constant.dart';
import 'other.dart';
import 'window.dart';
class Tray {
Future _updateSystemTray({
required Brightness? brightness,
bool force = false,
}) async {
if (Platform.isAndroid) {
return;
}
if (Platform.isLinux || force) {
await trayManager.destroy();
}
await trayManager.setIcon(
other.getTrayIconPath(
brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness,
),
isTemplate: true,
);
if (!Platform.isLinux) {
await trayManager.setToolTip(
appName,
);
}
}
update({
required AppState appState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
bool focus = false,
}) async {
if (Platform.isAndroid) {
return;
}
if (!Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
force: focus,
);
}
List<MenuItem> menuItems = [];
final showMenuItem = MenuItem(
label: appLocalizations.show,
onClick: (_) {
window?.show();
},
);
menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async {
globalState.appController.updateStart();
},
checked: false,
);
menuItems.add(startMenuItem);
menuItems.add(MenuItem.separator());
for (final mode in Mode.values) {
menuItems.add(
MenuItem.checkbox(
label: Intl.message(mode.name),
onClick: (_) {
globalState.appController.clashConfig.mode = mode;
},
checked: mode == clashConfig.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (!Platform.isWindows) {
final groups = appState.currentGroups;
for (final group in groups) {
List<MenuItem> subMenuItems = [];
for (final proxy in group.all) {
subMenuItems.add(
MenuItem.checkbox(
label: proxy.name,
checked: appState.selectedMap[group.name] == proxy.name,
onClick: (_) {
final appController = globalState.appController;
appController.config.updateCurrentSelectedMap(
group.name,
proxy.name,
);
appController.changeProxy(
groupName: group.name,
proxyName: proxy.name,
);
},
),
);
}
menuItems.add(
MenuItem.submenu(
label: group.name,
submenu: Menu(
items: subMenuItems,
),
),
);
}
if (groups.isNotEmpty) {
menuItems.add(MenuItem.separator());
}
}
if (appFlowingState.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
globalState.appController.updateTun();
},
checked: clashConfig.tun.enable,
),
);
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.systemProxy,
onClick: (_) {
globalState.appController.updateSystemProxy();
},
checked: config.networkProps.systemProxy,
),
);
menuItems.add(MenuItem.separator());
}
final autoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.autoLaunch,
onClick: (_) async {
globalState.appController.updateAutoLaunch();
},
checked: config.appSetting.autoLaunch,
);
final copyEnvVarMenuItem = MenuItem(
label: appLocalizations.copyEnvVar,
onClick: (_) async {
await _copyEnv(clashConfig.mixedPort);
},
);
menuItems.add(autoStartMenuItem);
menuItems.add(copyEnvVarMenuItem);
menuItems.add(MenuItem.separator());
final exitMenuItem = MenuItem(
label: appLocalizations.exit,
onClick: (_) async {
await globalState.appController.handleExit();
},
);
menuItems.add(exitMenuItem);
final menu = Menu(items: menuItems);
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
force: focus,
);
}
}
Future<void> _copyEnv(int port) async {
final url = "http://127.0.0.1:$port";
final cmdline = Platform.isWindows
? "set \$env:all_proxy=$url"
: "export all_proxy=$url";
await Clipboard.setData(
ClipboardData(
text: cmdline,
),
);
}
}
final tray = Tray();

View File

@@ -4,12 +4,14 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'package:windows_single_instance/windows_single_instance.dart';
class Window {
init(WindowProps props, int version) async {
final acquire = await singleInstanceLock.acquire();
if (!acquire) {
exit(0);
}
if (Platform.isWindows) {
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
protocol.register("clash");
protocol.register("clashmeta");
protocol.register("flclash");

View File

@@ -1,7 +1,10 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/cupertino.dart';
import 'package:path/path.dart';
class Windows {
@@ -51,12 +54,84 @@ class Windows {
calloc.free(argumentsPtr);
calloc.free(operationPtr);
if (result <= 32) {
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
if (result < 42) {
return false;
}
return true;
}
_killProcess(int port) async {
final result = await Process.run('netstat', ['-ano']);
final lines = result.stdout.toString().trim().split('\n');
for (final line in lines) {
if (!line.contains(":$port") || !line.contains("LISTENING")) {
continue;
}
final parts = line.trim().split(RegExp(r'\s+'));
final pid = int.tryParse(parts.last);
if (pid != null) {
await Process.run('taskkill', ['/PID', pid.toString(), '/F']);
}
}
}
Future<WindowsHelperServiceStatus> checkService() async {
// final qcResult = await Process.run('sc', ['qc', appHelperService]);
// final qcOutput = qcResult.stdout.toString();
// if (qcResult.exitCode != 0 || !qcOutput.contains(appPath.helperPath)) {
// return WindowsHelperServiceStatus.none;
// }
final result = await Process.run('sc', ['query', appHelperService]);
if(result.exitCode != 0){
return WindowsHelperServiceStatus.none;
}
final output = result.stdout.toString();
if (output.contains("RUNNING") && await request.pingHelper()) {
return WindowsHelperServiceStatus.running;
}
return WindowsHelperServiceStatus.presence;
}
Future<bool> registerService() async {
final status = await checkService();
if (status == WindowsHelperServiceStatus.running) {
return true;
}
await _killProcess(helperPort);
final command = [
"/c",
if (status == WindowsHelperServiceStatus.presence) ...[
"sc",
"delete",
appHelperService,
"/force",
"&&",
],
"sc",
"create",
appHelperService,
'binPath= "${appPath.helperPath}"',
'start= auto',
"&&",
"sc",
"start",
appHelperService,
].join(" ");
final res = runas("cmd.exe", command);
await Future.delayed(
Duration(milliseconds: 300),
);
return res;
}
Future<bool> registerTask(String appName) async {
final taskXml = '''
<?xml version="1.0" encoding="UTF-16"?>

View File

@@ -5,6 +5,7 @@ import 'dart:isolate';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
@@ -13,7 +14,6 @@ import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'clash/core.dart';
import 'common/common.dart';
import 'models/models.dart';
@@ -28,6 +28,7 @@ class AppController {
late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
late Function savePreferencesDebounce;
late Function changeProxyDebounce;
AppController(this.context) {
appState = context.read<AppState>();
@@ -43,6 +44,13 @@ class AppController {
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
changeProxyDebounce = debounce((String groupName, String proxyName) async {
await changeProxy(
groupName: groupName,
proxyName: proxyName,
);
await updateGroups();
});
addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++;
});
@@ -51,6 +59,14 @@ class AppController {
});
}
restartCore() async {
await globalState.restartCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
}
updateStatus(bool isStart) async {
if (isStart) {
await globalState.handleStart();
@@ -60,23 +76,31 @@ class AppController {
updateRunTime,
updateTraffic,
];
if (!Platform.isAndroid) {
applyProfileDebounce();
final currentLastModified =
await config.getCurrentProfile()?.profileLastModified;
if (currentLastModified == null ||
globalState.lastProfileModified == null) {
addCheckIpNumDebounce();
return;
}
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
addCheckIpNumDebounce();
return;
}
applyProfileDebounce();
} else {
await globalState.handleStop();
clashCore.resetTraffic();
await clashCore.resetTraffic();
appFlowingState.traffics = [];
appFlowingState.totalTraffic = Traffic();
appFlowingState.runTime = null;
await Future.delayed(
Duration(milliseconds: 300),
);
addCheckIpNumDebounce();
}
}
updateCoreVersionInfo() {
globalState.updateCoreVersionInfo(appState);
}
updateRunTime() {
final startTime = globalState.startTime;
if (startTime != null) {
@@ -90,6 +114,7 @@ class AppController {
updateTraffic() {
globalState.updateTraffic(
config: config,
appFlowingState: appFlowingState,
);
}
@@ -102,7 +127,7 @@ class AppController {
deleteProfile(String id) async {
config.deleteProfileById(id);
clashCore.clearEffect(id);
clearEffect(id);
if (config.currentProfileId == id) {
if (config.profiles.isNotEmpty) {
final updateId = config.profiles.first.id;
@@ -130,6 +155,7 @@ class AppController {
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await globalState.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
@@ -213,8 +239,8 @@ class AppController {
changeProxy({
required String groupName,
required String proxyName,
}) {
globalState.changeProxy(
}) async {
await globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
@@ -234,22 +260,16 @@ class AppController {
}
handleExit() async {
await updateStatus(false);
await proxy?.stopProxy();
await savePreferences();
clashCore.shutdown();
try {
await updateStatus(false);
await clashCore.shutdown();
await clashService?.destroy();
await proxy?.stopProxy();
await savePreferences();
} catch (_) {}
system.exit();
}
updateLogStatus() {
if (config.appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
appFlowingState.logs = [];
}
}
autoCheckUpdate() async {
if (!config.appSetting.autoCheckUpdate) return;
final res = await request.checkForUpdate();
@@ -304,10 +324,20 @@ class AppController {
if (!isDisclaimerAccepted) {
handleExit();
}
updateLogStatus();
if (!config.appSetting.silentLaunch) {
window?.show();
}
await globalState.initCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
await _initStatus();
autoUpdateProfiles();
autoCheckUpdate();
}
_initStatus() async {
if (Platform.isAndroid) {
globalState.updateStartTime();
}
@@ -316,8 +346,6 @@ class AppController {
} else {
await updateStatus(config.appSetting.autoRun);
}
autoUpdateProfiles();
autoCheckUpdate();
}
setDelay(Delay delay) {
@@ -525,6 +553,19 @@ class AppController {
'';
}
clearEffect(String profileId) async {
final profilePath = await appPath.getProfilePath(profileId);
final providersPath = await appPath.getProvidersPath(profileId);
return await Isolate.run(() async {
if (profilePath != null) {
await File(profilePath).delete(recursive: true);
}
if (providersPath != null) {
await File(providersPath).delete(recursive: true);
}
});
}
updateTun() {
clashConfig.tun = clashConfig.tun.copyWith(
enable: !clashConfig.tun.enable,
@@ -547,12 +588,6 @@ class AppController {
);
}
updateAdminAutoLaunch() {
config.appSetting = config.appSetting.copyWith(
adminAutoLaunch: !config.appSetting.adminAutoLaunch,
);
}
updateVisible() async {
final visible = await window?.isVisible();
if (visible != null && !visible) {
@@ -602,7 +637,7 @@ class AppController {
}
updateTray([bool focus = false]) async {
globalState.updateTray(
tray.update(
appState: appState,
appFlowingState: appFlowingState,
config: config,

View File

@@ -178,3 +178,39 @@ enum RouteMode {
bypassPrivate,
config,
}
enum ActionMethod {
message,
initClash,
getIsInit,
forceGc,
shutdown,
validateConfig,
updateConfig,
getProxies,
changeProxy,
getTraffic,
getTotalTraffic,
resetTraffic,
asyncTestDelay,
getConnections,
closeConnections,
closeConnection,
getExternalProviders,
getExternalProvider,
updateGeoData,
updateExternalProvider,
sideLoadExternalProvider,
startLog,
stopLog,
startListener,
stopListener,
}
enum AuthorizeCode { none, success, error }
enum WindowsHelperServiceStatus {
none,
presence,
running,
}

View File

@@ -60,32 +60,6 @@ class UsageSwitch extends StatelessWidget {
}
}
class AdminAutoLaunchItem extends StatelessWidget {
const AdminAutoLaunchItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.adminAutoLaunch,
builder: (_, adminAutoLaunch, __) {
return ListItem.switchItem(
title: Text(appLocalizations.adminAutoLaunch),
subtitle: Text(appLocalizations.adminAutoLaunchDesc),
delegate: SwitchDelegate(
value: adminAutoLaunch,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
adminAutoLaunch: value,
);
},
),
);
},
);
}
}
class ApplicationSettingFragment extends StatelessWidget {
const ApplicationSettingFragment({super.key});
@@ -134,8 +108,6 @@ class ApplicationSettingFragment extends StatelessWidget {
);
},
),
if(Platform.isWindows)
const AdminAutoLaunchItem(),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.silentLaunch,

View File

@@ -27,18 +27,19 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
WidgetsBinding.instance.addPostFrameCallback((_) async {
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
(timer) async {
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
connections: await clashCore.getConnections(),
);
},
);
@@ -66,10 +67,10 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
width: 8,
),
IconButton(
onPressed: () {
onPressed: () async {
clashCore.closeConnections();
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
connections: await clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
@@ -99,10 +100,11 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
);
}
_handleBlockConnection(String id) {
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
}
@override
@@ -239,10 +241,10 @@ class ConnectionsSearchDelegate extends SearchDelegate {
);
}
_handleBlockConnection(String id) {
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
connections: await clashCore.getConnections(),
);
}

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:math';
import 'package:fl_clash/common/common.dart';
@@ -68,11 +67,10 @@ class _DashboardFragmentState extends State<DashboardFragment> {
// child: const VPNSwitch(),
// ),
if (system.isDesktop) ...[
if (Platform.isWindows)
GridItem(
crossAxisCellCount: switchCount,
child: const TUNButton(),
),
GridItem(
crossAxisCellCount: switchCount,
child: const TUNButton(),
),
GridItem(
crossAxisCellCount: switchCount,
child: const SystemProxyButton(),

View File

@@ -9,6 +9,13 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
class NetworkDetection extends StatefulWidget {
const NetworkDetection({super.key});
@@ -17,12 +24,6 @@ class NetworkDetection extends StatefulWidget {
}
class _NetworkDetectionState extends State<NetworkDetection> {
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
bool? _preIsStart;
Function? _checkIpDebounce;
Timer? _setTimeoutTimer;
@@ -55,17 +56,20 @@ class _NetworkDetectionState extends State<NetworkDetection> {
);
return;
}
_setTimeoutTimer = Timer(const Duration(milliseconds: 2000), () {
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: null,
);
});
} catch (_) {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
} catch (e) {
if (e.toString() == "cancelled") {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
}
}
}
@@ -92,9 +96,8 @@ class _NetworkDetectionState extends State<NetworkDetection> {
}
@override
void dispose() {
dispose() {
super.dispose();
networkDetectionState.dispose();
}
String countryCodeToEmoji(String countryCode) {
@@ -156,7 +159,8 @@ class _NetworkDetectionState extends State<NetworkDetection> {
.textTheme
.titleLarge
?.copyWith(
fontFamily: FontFamily.twEmoji.value,
fontFamily:
FontFamily.twEmoji.value,
),
),
)

View File

@@ -115,11 +115,10 @@ class ProxyCard extends StatelessWidget {
groupName,
nextProxyName,
);
appController.changeProxy(
groupName: groupName,
proxyName: nextProxyName,
);
await appController.updateGroupDebounce();
await appController.changeProxyDebounce([
groupName,
nextProxyName,
]);
return;
}
globalState.showSnackBar(

View File

@@ -4,7 +4,7 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart';
import 'package:fl_clash/models/ffi.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
@@ -56,7 +56,7 @@ class _ProvidersState extends State<Providers> {
providerName: provider.name,
);
appState.setProvider(
clashCore.getExternalProvider(provider.name),
await clashCore.getExternalProvider(provider.name),
);
},
);
@@ -122,7 +122,7 @@ class ProviderItem extends StatelessWidget {
if (message.isNotEmpty) throw message;
});
appState.setProvider(
clashCore.getExternalProvider(provider.name),
await clashCore.getExternalProvider(provider.name),
);
});
await globalState.appController.updateGroupDebounce();
@@ -143,7 +143,7 @@ class ProviderItem extends StatelessWidget {
);
if (message.isNotEmpty) throw message;
appState.setProvider(
clashCore.getExternalProvider(provider.name),
await clashCore.getExternalProvider(provider.name),
);
if (message.isNotEmpty) throw message;
});

View File

@@ -1,4 +1,5 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
@@ -10,6 +11,8 @@ import 'package:provider/provider.dart';
import 'card.dart';
import 'common.dart';
List<Proxy> currentProxies = [];
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
class ProxiesTabFragment extends StatefulWidget {
@@ -28,7 +31,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
@override
void dispose() {
super.dispose();
_tabController?.dispose();
_destroyTabController();
}
scrollToGroupSelected() {
@@ -106,6 +109,36 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
);
}
_tabControllerListener([int? index]) {
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
if (_tabController?.index == null) {
return;
}
final currentGroup = currentGroups[index ?? _tabController!.index];
currentProxies = currentGroup.all;
appController.config.updateCurrentGroupName(
currentGroup.name,
);
}
_destroyTabController() {
_tabController?.removeListener(_tabControllerListener);
_tabController?.dispose();
_tabController = null;
}
_updateTabController(int length, int index) {
final realIndex = index == -1 ? 0 : index;
_tabController ??= TabController(
length: length,
initialIndex: realIndex,
vsync: this,
);
_tabControllerListener(realIndex);
_tabController?.addListener(_tabControllerListener);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
@@ -119,8 +152,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
},
shouldRebuild: (prev, next) {
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
_tabController?.dispose();
_tabController = null;
_destroyTabController();
return true;
}
return false;
@@ -129,12 +161,8 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_tabController ??= TabController(
length: state.groupNames.length,
initialIndex: index == -1 ? 0 : index,
vsync: this,
);
GroupNameKeyMap keyMap = {};
_updateTabController(state.groupNames.length, index);
final GroupNameKeyMap keyMap = {};
final children = state.groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName);
return KeepScope(
@@ -167,16 +195,6 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
left: 16,
right: 16 + (value ? 16 : 0),
),
onTap: (index) {
final appController = globalState.appController;
final currentGroups =
appController.appState.currentGroups;
if (currentGroups.length > index) {
appController.config.updateCurrentGroupName(
currentGroups[index].name,
);
}
},
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
@@ -243,14 +261,13 @@ class ProxyGroupView extends StatefulWidget {
class ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false;
final _controller = ScrollController();
List<Proxy> _lastProxies = [];
String get groupName => widget.groupName;
_delayTest(List<Proxy> proxies) async {
_delayTest() async {
if (isLock) return;
isLock = true;
await delayTest(proxies);
await delayTest(currentProxies);
isLock = false;
}
@@ -269,7 +286,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
16 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: _lastProxies,
proxies: currentProxies,
),
_controller.position.maxScrollExtent,
),
@@ -278,7 +295,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
);
}
initFab(bool isCurrent, List<Proxy> proxies) {
initFab(bool isCurrent) {
if (!isCurrent) {
return;
}
@@ -287,9 +304,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async {
await _delayTest(
proxies,
);
await _delayTest();
},
);
});
@@ -319,11 +334,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
_lastProxies = sortedProxies;
return ActiveBuilder(
label: "proxies",
builder: (isCurrent, child) {
initFab(isCurrent, proxies);
initFab(isCurrent);
return child!;
},
child: Align(
@@ -381,7 +395,9 @@ class _DelayTestButtonState extends State<DelayTestButton>
_healthcheck() async {
_controller.forward();
await widget.onClick();
_controller.reverse();
if (mounted) {
_controller.reverse();
}
}
@override

View File

@@ -330,5 +330,7 @@
"routeMode_bypassPrivate": "Bypass private route address",
"routeMode_config": "Use config",
"routeAddress": "Route address",
"routeAddressDesc": "Config listen route address"
"routeAddressDesc": "Config listen route address",
"pleaseInputAdminPassword": "Please enter the admin password",
"copyEnvVar": "Copying environment variables"
}

View File

@@ -330,5 +330,7 @@
"routeMode_bypassPrivate": "绕过私有路由地址",
"routeMode_config": "使用配置",
"routeAddress": "路由地址",
"routeAddressDesc": "配置监听路由地址"
"routeAddressDesc": "配置监听路由地址",
"pleaseInputAdminPassword": "请输入管理员密码",
"copyEnvVar": "复制环境变量"
}

View File

@@ -121,6 +121,8 @@ class MessageLookup extends MessageLookupByLibrary {
"View current connections data"),
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage(
"Copying environment variables"),
"core": MessageLookupByLibrary.simpleMessage("Core"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
"country": MessageLookupByLibrary.simpleMessage("Country"),
@@ -326,6 +328,8 @@ class MessageLookup extends MessageLookupByLibrary {
"paste": MessageLookupByLibrary.simpleMessage("Paste"),
"pleaseBindWebDAV":
MessageLookupByLibrary.simpleMessage("Please bind WebDAV"),
"pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
"Please enter the admin password"),
"pleaseUploadFile":
MessageLookupByLibrary.simpleMessage("Please upload file"),
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(

View File

@@ -98,6 +98,7 @@ class MessageLookup extends MessageLookupByLibrary {
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copy": MessageLookupByLibrary.simpleMessage("复制"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
"core": MessageLookupByLibrary.simpleMessage("内核"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"country": MessageLookupByLibrary.simpleMessage("区域"),
@@ -259,6 +260,8 @@ class MessageLookup extends MessageLookupByLibrary {
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
"pleaseInputAdminPassword":
MessageLookupByLibrary.simpleMessage("请输入管理员密码"),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
"pleaseUploadValidQrcode":
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),

View File

@@ -3369,6 +3369,26 @@ class AppLocalizations {
args: [],
);
}
/// `Please enter the admin password`
String get pleaseInputAdminPassword {
return Intl.message(
'Please enter the admin password',
name: 'pleaseInputAdminPassword',
desc: '',
args: [],
);
}
/// `Copying environment variables`
String get copyEnvVar {
return Intl.message(
'Copying environment variables',
name: 'copyEnvVar',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -8,14 +8,15 @@ import 'package:fl_clash/plugins/vpn.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'application.dart';
import 'common/common.dart';
import 'l10n/l10n.dart';
import 'models/models.dart';
import 'common/common.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
clashCore.initMessage();
clashLib?.initMessage();
globalState.packageInfo = await PackageInfo.fromPlatform();
final version = await system.version;
final config = await preferences.getConfig() ?? Config();
@@ -36,17 +37,12 @@ Future<void> main() async {
openLogs: config.appSetting.openLogs,
hasProxies: false,
);
globalState.updateTray(
tray.update(
appState: appState,
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
);
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
HttpOverrides.global = FlClashHttpOverrides();
runAppWithPreferences(
const Application(),
@@ -69,39 +65,66 @@ Future<void> vpnService() async {
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
final appState = AppState(
mode: clashConfig.mode,
selectedMap: config.currentSelectedMap,
version: version,
);
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
await app?.tip(appLocalizations.startVpn);
globalState
.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: false,
)
.then(
(_) async {
await globalState.handleStart();
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.handleStop();
clashCore.shutdown();
exit(0);
},
),
);
globalState.updateTraffic(config: config);
globalState.updateFunctionLists = [
() {
globalState.updateTraffic(config: config);
}
];
},
);
vpn?.setServiceMessageHandler(
ServiceMessageHandler(
onProtect: (Fd fd) async {
await vpn?.setProtect(fd.value);
clashCore.setFdMap(fd.id);
clashLib?.setFdMap(fd.id);
},
onProcess: (Process process) async {
onProcess: (ProcessData process) async {
final packageName = await vpn?.resolverProcess(process);
clashCore.setProcessMap(
clashLib?.setProcessMap(
ProcessMapItem(
id: process.id,
value: packageName ?? "",
),
);
},
onStarted: (String runTime) async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
},
onLoaded: (String groupName) {
final currentSelectedMap = config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
@@ -114,43 +137,20 @@ Future<void> vpnService() async {
},
),
);
await app?.tip(appLocalizations.startVpn);
await globalState.handleStart();
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.handleStop();
clashCore.shutdown();
exit(0);
},
),
);
globalState.updateTraffic();
globalState.updateFunctionLists = [
() {
globalState.updateTraffic();
}
];
}
@immutable
class ServiceMessageHandler with ServiceMessageListener {
final Function(Fd fd) _onProtect;
final Function(Process process) _onProcess;
final Function(String runTime) _onStarted;
final Function(ProcessData process) _onProcess;
final Function(String providerName) _onLoaded;
const ServiceMessageHandler({
required Function(Fd fd) onProtect,
required Function(Process process) onProcess,
required Function(String runTime) onStarted,
required Function(ProcessData process) onProcess,
required Function(String providerName) onLoaded,
}) : _onProtect = onProtect,
_onProcess = onProcess,
_onStarted = onStarted,
_onLoaded = onLoaded;
@override
@@ -159,15 +159,10 @@ class ServiceMessageHandler with ServiceMessageListener {
}
@override
onProcess(Process process) {
onProcess(ProcessData process) {
_onProcess(process);
}
@override
onStarted(String runTime) {
_onStarted(runTime);
}
@override
onLoaded(String providerName) {
_onLoaded(providerName);

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:flutter/material.dart';
@@ -23,6 +24,28 @@ class _AndroidContainerState extends State<AndroidManager> {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
Widget _updateCoreState(Widget child) {
return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
bypassDomain: config.networkProps.bypassDomain,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
routeAddress: clashConfig.routeAddress,
),
builder: (__, state, child) {
clashLib?.setState(state);
return child!;
},
child: child,
);
}
Widget _excludeContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.hidden,
@@ -36,6 +59,10 @@ class _AndroidContainerState extends State<AndroidManager> {
@override
Widget build(BuildContext context) {
return _excludeContainer(widget.child);
return _updateCoreState(
_excludeContainer(
widget.child,
),
);
}
}

View File

@@ -57,41 +57,20 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
);
}
Widget _updateCoreState(Widget child) {
return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
bypassDomain: config.networkProps.bypassDomain,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
routeAddress: clashConfig.routeAddress,
),
builder: (__, state, child) {
clashCore.setState(state);
return child!;
},
child: child,
);
}
_changeProfile() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final appController = globalState.appController;
appController.appState.delayMap = {};
await appController.applyProfile();
});
}
Widget _changeProfileContainer(Widget child) {
return Selector<Config, String?>(
selector: (_, config) => config.currentProfileId,
shouldRebuild: (prev, next) {
if (prev != next) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final appController = globalState.appController;
appController.appState.delayMap = {};
appController.applyProfile();
});
}
return prev != next;
},
builder: (__, state, child) {
_changeProfile();
return child!;
},
child: child,
@@ -101,10 +80,8 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
@override
Widget build(BuildContext context) {
return _changeProfileContainer(
_updateCoreState(
_updateContainer(
widget.child,
),
_updateContainer(
widget.child,
),
);
}
@@ -158,7 +135,7 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
Future<void> onLoaded(String providerName) async {
final appController = globalState.appController;
appController.appState.setProvider(
clashCore.getExternalProvider(
await clashCore.getExternalProvider(
providerName,
),
);

View File

@@ -30,13 +30,15 @@ class _TrayContainerState extends State<TrayManager> with TrayListener {
selector: (_, appState, appFlowingState, config, clashConfig) =>
TrayState(
mode: clashConfig.mode,
adminAutoLaunch: config.appSetting.adminAutoLaunch,
autoLaunch: config.appSetting.autoLaunch,
isStart: appFlowingState.isStart,
locale: config.appSetting.locale,
systemProxy: config.networkProps.systemProxy,
tunEnable: clashConfig.tun.enable,
brightness: appState.brightness,
port: clashConfig.mixedPort,
groups: appState.groups,
map: appState.selectedMap,
),
shouldRebuild: (prev, next) {
if (prev != next) {

View File

@@ -28,7 +28,6 @@ class _WindowContainerState extends State<WindowManager>
return Selector<Config, AutoLaunchState>(
selector: (_, config) => AutoLaunchState(
isAutoLaunch: config.appSetting.autoLaunch,
isAdminAutoLaunch: config.appSetting.adminAutoLaunch,
),
builder: (_, state, child) {
updateLaunchDebounce ??= debounce((AutoLaunchState state) {
@@ -88,8 +87,9 @@ class _WindowContainerState extends State<WindowManager>
}
@override
void onTaskbarCreated() {
Future<void> onTaskbarCreated() async {
globalState.appController.updateTray(true);
await globalState.appController.restartCore();
super.onTaskbarCreated();
}

View File

@@ -1,8 +1,9 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'common.dart';
import 'ffi.dart';
import 'core.dart';
import 'profile.dart';
typedef DelayMap = Map<String, int?>;

View File

@@ -482,6 +482,28 @@ class ClashConfig extends ChangeNotifier {
notifyListeners();
}
ClashConfig copyWith() {
return ClashConfig()
..mixedPort = _mixedPort
..mode = _mode
..ipv6 = _ipv6
..findProcessMode = _findProcessMode
..allowLan = _allowLan
..tcpConcurrent = _tcpConcurrent
..logLevel = _logLevel
..tun = tun
..unifiedDelay = _unifiedDelay
..geodataLoader = _geodataLoader
..externalController = _externalController
..keepAliveInterval = _keepAliveInterval
..dns = _dns
..geoXUrl = _geoXUrl
..routeMode = _routeMode
..includeRouteAddress = _includeRouteAddress
..rules = _rules
..hosts = _hosts;
}
Map<String, dynamic> toJson() {
return _$ClashConfigToJson(this);
}

View File

@@ -20,7 +20,6 @@ class AppSetting with _$AppSetting {
String? locale,
@Default(false) bool onlyProxy,
@Default(false) bool autoLaunch,
@Default(false) bool adminAutoLaunch,
@Default(false) bool silentLaunch,
@Default(false) bool autoRun,
@Default(false) bool openLogs,

View File

@@ -1,11 +1,35 @@
// ignore_for_file: invalid_annotation_target
import 'dart:convert';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/ffi.freezed.dart';
part 'generated/ffi.g.dart';
part 'generated/core.freezed.dart';
part 'generated/core.g.dart';
abstract mixin class AppMessageListener {
void onLog(Log log) {}
void onDelay(Delay delay) {}
void onRequest(Connection connection) {}
void onStarted(String runTime) {}
void onLoaded(String providerName) {}
}
abstract mixin class ServiceMessageListener {
onProtect(Fd fd) {}
onProcess(ProcessData process) {}
onStarted(String runTime) {}
onLoaded(String providerName) {}
}
@freezed
class CoreState with _$CoreState {
@@ -124,14 +148,14 @@ class Now with _$Now {
}
@freezed
class Process with _$Process {
const factory Process({
class ProcessData with _$ProcessData {
const factory ProcessData({
required int id,
required Metadata metadata,
}) = _Process;
}) = _ProcessData;
factory Process.fromJson(Map<String, Object?> json) =>
_$ProcessFromJson(json);
factory ProcessData.fromJson(Map<String, Object?> json) =>
_$ProcessDataFromJson(json);
}
@freezed
@@ -212,24 +236,19 @@ class TunProps with _$TunProps {
_$TunPropsFromJson(json);
}
abstract mixin class AppMessageListener {
void onLog(Log log) {}
@freezed
class Action with _$Action {
const factory Action({
required ActionMethod method,
required dynamic data,
required String id,
}) = _Action;
void onDelay(Delay delay) {}
void onRequest(Connection connection) {}
void onStarted(String runTime) {}
void onLoaded(String providerName) {}
factory Action.fromJson(Map<String, Object?> json) => _$ActionFromJson(json);
}
abstract mixin class ServiceMessageListener {
onProtect(Fd fd) {}
onProcess(Process process) {}
onStarted(String runTime) {}
onLoaded(String providerName) {}
extension ActionExt on Action {
String get toJson {
return json.encode(this);
}
}

View File

@@ -23,7 +23,6 @@ mixin _$AppSetting {
String? get locale => throw _privateConstructorUsedError;
bool get onlyProxy => throw _privateConstructorUsedError;
bool get autoLaunch => throw _privateConstructorUsedError;
bool get adminAutoLaunch => throw _privateConstructorUsedError;
bool get silentLaunch => throw _privateConstructorUsedError;
bool get autoRun => throw _privateConstructorUsedError;
bool get openLogs => throw _privateConstructorUsedError;
@@ -56,7 +55,6 @@ abstract class $AppSettingCopyWith<$Res> {
{String? locale,
bool onlyProxy,
bool autoLaunch,
bool adminAutoLaunch,
bool silentLaunch,
bool autoRun,
bool openLogs,
@@ -88,7 +86,6 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
Object? locale = freezed,
Object? onlyProxy = null,
Object? autoLaunch = null,
Object? adminAutoLaunch = null,
Object? silentLaunch = null,
Object? autoRun = null,
Object? openLogs = null,
@@ -114,10 +111,6 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
? _value.autoLaunch
: autoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
adminAutoLaunch: null == adminAutoLaunch
? _value.adminAutoLaunch
: adminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
silentLaunch: null == silentLaunch
? _value.silentLaunch
: silentLaunch // ignore: cast_nullable_to_non_nullable
@@ -178,7 +171,6 @@ abstract class _$$AppSettingImplCopyWith<$Res>
{String? locale,
bool onlyProxy,
bool autoLaunch,
bool adminAutoLaunch,
bool silentLaunch,
bool autoRun,
bool openLogs,
@@ -208,7 +200,6 @@ class __$$AppSettingImplCopyWithImpl<$Res>
Object? locale = freezed,
Object? onlyProxy = null,
Object? autoLaunch = null,
Object? adminAutoLaunch = null,
Object? silentLaunch = null,
Object? autoRun = null,
Object? openLogs = null,
@@ -234,10 +225,6 @@ class __$$AppSettingImplCopyWithImpl<$Res>
? _value.autoLaunch
: autoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
adminAutoLaunch: null == adminAutoLaunch
? _value.adminAutoLaunch
: adminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
silentLaunch: null == silentLaunch
? _value.silentLaunch
: silentLaunch // ignore: cast_nullable_to_non_nullable
@@ -293,7 +280,6 @@ class _$AppSettingImpl implements _AppSetting {
{this.locale,
this.onlyProxy = false,
this.autoLaunch = false,
this.adminAutoLaunch = false,
this.silentLaunch = false,
this.autoRun = false,
this.openLogs = false,
@@ -319,9 +305,6 @@ class _$AppSettingImpl implements _AppSetting {
final bool autoLaunch;
@override
@JsonKey()
final bool adminAutoLaunch;
@override
@JsonKey()
final bool silentLaunch;
@override
@JsonKey()
@@ -356,7 +339,7 @@ class _$AppSettingImpl implements _AppSetting {
@override
String toString() {
return 'AppSetting(locale: $locale, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, adminAutoLaunch: $adminAutoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)';
return 'AppSetting(locale: $locale, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)';
}
@override
@@ -369,8 +352,6 @@ class _$AppSettingImpl implements _AppSetting {
other.onlyProxy == onlyProxy) &&
(identical(other.autoLaunch, autoLaunch) ||
other.autoLaunch == autoLaunch) &&
(identical(other.adminAutoLaunch, adminAutoLaunch) ||
other.adminAutoLaunch == adminAutoLaunch) &&
(identical(other.silentLaunch, silentLaunch) ||
other.silentLaunch == silentLaunch) &&
(identical(other.autoRun, autoRun) || other.autoRun == autoRun) &&
@@ -399,7 +380,6 @@ class _$AppSettingImpl implements _AppSetting {
locale,
onlyProxy,
autoLaunch,
adminAutoLaunch,
silentLaunch,
autoRun,
openLogs,
@@ -433,7 +413,6 @@ abstract class _AppSetting implements AppSetting {
{final String? locale,
final bool onlyProxy,
final bool autoLaunch,
final bool adminAutoLaunch,
final bool silentLaunch,
final bool autoRun,
final bool openLogs,
@@ -456,8 +435,6 @@ abstract class _AppSetting implements AppSetting {
@override
bool get autoLaunch;
@override
bool get adminAutoLaunch;
@override
bool get silentLaunch;
@override
bool get autoRun;

View File

@@ -56,7 +56,6 @@ _$AppSettingImpl _$$AppSettingImplFromJson(Map<String, dynamic> json) =>
locale: json['locale'] as String?,
onlyProxy: json['onlyProxy'] as bool? ?? false,
autoLaunch: json['autoLaunch'] as bool? ?? false,
adminAutoLaunch: json['adminAutoLaunch'] as bool? ?? false,
silentLaunch: json['silentLaunch'] as bool? ?? false,
autoRun: json['autoRun'] as bool? ?? false,
openLogs: json['openLogs'] as bool? ?? false,
@@ -75,7 +74,6 @@ Map<String, dynamic> _$$AppSettingImplToJson(_$AppSettingImpl instance) =>
'locale': instance.locale,
'onlyProxy': instance.onlyProxy,
'autoLaunch': instance.autoLaunch,
'adminAutoLaunch': instance.adminAutoLaunch,
'silentLaunch': instance.silentLaunch,
'autoRun': instance.autoRun,
'openLogs': instance.openLogs,

View File

@@ -3,7 +3,7 @@
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of '../ffi.dart';
part of '../core.dart';
// **************************************************************************
// FreezedGenerator
@@ -2080,28 +2080,30 @@ abstract class _Now implements Now {
throw _privateConstructorUsedError;
}
Process _$ProcessFromJson(Map<String, dynamic> json) {
return _Process.fromJson(json);
ProcessData _$ProcessDataFromJson(Map<String, dynamic> json) {
return _ProcessData.fromJson(json);
}
/// @nodoc
mixin _$Process {
mixin _$ProcessData {
int get id => throw _privateConstructorUsedError;
Metadata get metadata => throw _privateConstructorUsedError;
/// Serializes this Process to a JSON map.
/// Serializes this ProcessData to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ProcessCopyWith<Process> get copyWith => throw _privateConstructorUsedError;
$ProcessDataCopyWith<ProcessData> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProcessCopyWith<$Res> {
factory $ProcessCopyWith(Process value, $Res Function(Process) then) =
_$ProcessCopyWithImpl<$Res, Process>;
abstract class $ProcessDataCopyWith<$Res> {
factory $ProcessDataCopyWith(
ProcessData value, $Res Function(ProcessData) then) =
_$ProcessDataCopyWithImpl<$Res, ProcessData>;
@useResult
$Res call({int id, Metadata metadata});
@@ -2109,16 +2111,16 @@ abstract class $ProcessCopyWith<$Res> {
}
/// @nodoc
class _$ProcessCopyWithImpl<$Res, $Val extends Process>
implements $ProcessCopyWith<$Res> {
_$ProcessCopyWithImpl(this._value, this._then);
class _$ProcessDataCopyWithImpl<$Res, $Val extends ProcessData>
implements $ProcessDataCopyWith<$Res> {
_$ProcessDataCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
@@ -2138,7 +2140,7 @@ class _$ProcessCopyWithImpl<$Res, $Val extends Process>
) as $Val);
}
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
@@ -2150,10 +2152,11 @@ class _$ProcessCopyWithImpl<$Res, $Val extends Process>
}
/// @nodoc
abstract class _$$ProcessImplCopyWith<$Res> implements $ProcessCopyWith<$Res> {
factory _$$ProcessImplCopyWith(
_$ProcessImpl value, $Res Function(_$ProcessImpl) then) =
__$$ProcessImplCopyWithImpl<$Res>;
abstract class _$$ProcessDataImplCopyWith<$Res>
implements $ProcessDataCopyWith<$Res> {
factory _$$ProcessDataImplCopyWith(
_$ProcessDataImpl value, $Res Function(_$ProcessDataImpl) then) =
__$$ProcessDataImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int id, Metadata metadata});
@@ -2163,14 +2166,14 @@ abstract class _$$ProcessImplCopyWith<$Res> implements $ProcessCopyWith<$Res> {
}
/// @nodoc
class __$$ProcessImplCopyWithImpl<$Res>
extends _$ProcessCopyWithImpl<$Res, _$ProcessImpl>
implements _$$ProcessImplCopyWith<$Res> {
__$$ProcessImplCopyWithImpl(
_$ProcessImpl _value, $Res Function(_$ProcessImpl) _then)
class __$$ProcessDataImplCopyWithImpl<$Res>
extends _$ProcessDataCopyWithImpl<$Res, _$ProcessDataImpl>
implements _$$ProcessDataImplCopyWith<$Res> {
__$$ProcessDataImplCopyWithImpl(
_$ProcessDataImpl _value, $Res Function(_$ProcessDataImpl) _then)
: super(_value, _then);
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
@@ -2178,7 +2181,7 @@ class __$$ProcessImplCopyWithImpl<$Res>
Object? id = null,
Object? metadata = null,
}) {
return _then(_$ProcessImpl(
return _then(_$ProcessDataImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
@@ -2193,11 +2196,11 @@ class __$$ProcessImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$ProcessImpl implements _Process {
const _$ProcessImpl({required this.id, required this.metadata});
class _$ProcessDataImpl implements _ProcessData {
const _$ProcessDataImpl({required this.id, required this.metadata});
factory _$ProcessImpl.fromJson(Map<String, dynamic> json) =>
_$$ProcessImplFromJson(json);
factory _$ProcessDataImpl.fromJson(Map<String, dynamic> json) =>
_$$ProcessDataImplFromJson(json);
@override
final int id;
@@ -2206,14 +2209,14 @@ class _$ProcessImpl implements _Process {
@override
String toString() {
return 'Process(id: $id, metadata: $metadata)';
return 'ProcessData(id: $id, metadata: $metadata)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProcessImpl &&
other is _$ProcessDataImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.metadata, metadata) ||
other.metadata == metadata));
@@ -2223,39 +2226,40 @@ class _$ProcessImpl implements _Process {
@override
int get hashCode => Object.hash(runtimeType, id, metadata);
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ProcessImplCopyWith<_$ProcessImpl> get copyWith =>
__$$ProcessImplCopyWithImpl<_$ProcessImpl>(this, _$identity);
_$$ProcessDataImplCopyWith<_$ProcessDataImpl> get copyWith =>
__$$ProcessDataImplCopyWithImpl<_$ProcessDataImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ProcessImplToJson(
return _$$ProcessDataImplToJson(
this,
);
}
}
abstract class _Process implements Process {
const factory _Process(
abstract class _ProcessData implements ProcessData {
const factory _ProcessData(
{required final int id,
required final Metadata metadata}) = _$ProcessImpl;
required final Metadata metadata}) = _$ProcessDataImpl;
factory _Process.fromJson(Map<String, dynamic> json) = _$ProcessImpl.fromJson;
factory _ProcessData.fromJson(Map<String, dynamic> json) =
_$ProcessDataImpl.fromJson;
@override
int get id;
@override
Metadata get metadata;
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ProcessImplCopyWith<_$ProcessImpl> get copyWith =>
_$$ProcessDataImplCopyWith<_$ProcessDataImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -3424,3 +3428,185 @@ abstract class _TunProps implements TunProps {
_$$TunPropsImplCopyWith<_$TunPropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}
Action _$ActionFromJson(Map<String, dynamic> json) {
return _Action.fromJson(json);
}
/// @nodoc
mixin _$Action {
ActionMethod get method => throw _privateConstructorUsedError;
dynamic get data => throw _privateConstructorUsedError;
String get id => throw _privateConstructorUsedError;
/// Serializes this Action to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ActionCopyWith<Action> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ActionCopyWith<$Res> {
factory $ActionCopyWith(Action value, $Res Function(Action) then) =
_$ActionCopyWithImpl<$Res, Action>;
@useResult
$Res call({ActionMethod method, dynamic data, String id});
}
/// @nodoc
class _$ActionCopyWithImpl<$Res, $Val extends Action>
implements $ActionCopyWith<$Res> {
_$ActionCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? method = null,
Object? data = freezed,
Object? id = null,
}) {
return _then(_value.copyWith(
method: null == method
? _value.method
: method // ignore: cast_nullable_to_non_nullable
as ActionMethod,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as dynamic,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ActionImplCopyWith<$Res> implements $ActionCopyWith<$Res> {
factory _$$ActionImplCopyWith(
_$ActionImpl value, $Res Function(_$ActionImpl) then) =
__$$ActionImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({ActionMethod method, dynamic data, String id});
}
/// @nodoc
class __$$ActionImplCopyWithImpl<$Res>
extends _$ActionCopyWithImpl<$Res, _$ActionImpl>
implements _$$ActionImplCopyWith<$Res> {
__$$ActionImplCopyWithImpl(
_$ActionImpl _value, $Res Function(_$ActionImpl) _then)
: super(_value, _then);
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? method = null,
Object? data = freezed,
Object? id = null,
}) {
return _then(_$ActionImpl(
method: null == method
? _value.method
: method // ignore: cast_nullable_to_non_nullable
as ActionMethod,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as dynamic,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ActionImpl implements _Action {
const _$ActionImpl(
{required this.method, required this.data, required this.id});
factory _$ActionImpl.fromJson(Map<String, dynamic> json) =>
_$$ActionImplFromJson(json);
@override
final ActionMethod method;
@override
final dynamic data;
@override
final String id;
@override
String toString() {
return 'Action(method: $method, data: $data, id: $id)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ActionImpl &&
(identical(other.method, method) || other.method == method) &&
const DeepCollectionEquality().equals(other.data, data) &&
(identical(other.id, id) || other.id == id));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, method, const DeepCollectionEquality().hash(data), id);
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ActionImplCopyWith<_$ActionImpl> get copyWith =>
__$$ActionImplCopyWithImpl<_$ActionImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ActionImplToJson(
this,
);
}
}
abstract class _Action implements Action {
const factory _Action(
{required final ActionMethod method,
required final dynamic data,
required final String id}) = _$ActionImpl;
factory _Action.fromJson(Map<String, dynamic> json) = _$ActionImpl.fromJson;
@override
ActionMethod get method;
@override
dynamic get data;
@override
String get id;
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ActionImplCopyWith<_$ActionImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of '../ffi.dart';
part of '../core.dart';
// **************************************************************************
// JsonSerializableGenerator
@@ -188,13 +188,13 @@ Map<String, dynamic> _$$NowImplToJson(_$NowImpl instance) => <String, dynamic>{
'value': instance.value,
};
_$ProcessImpl _$$ProcessImplFromJson(Map<String, dynamic> json) =>
_$ProcessImpl(
_$ProcessDataImpl _$$ProcessDataImplFromJson(Map<String, dynamic> json) =>
_$ProcessDataImpl(
id: (json['id'] as num).toInt(),
metadata: Metadata.fromJson(json['metadata'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
Map<String, dynamic> _$$ProcessDataImplToJson(_$ProcessDataImpl instance) =>
<String, dynamic>{
'id': instance.id,
'metadata': instance.metadata,
@@ -289,3 +289,44 @@ Map<String, dynamic> _$$TunPropsImplToJson(_$TunPropsImpl instance) =>
'dns': instance.dns,
'dns6': instance.dns6,
};
_$ActionImpl _$$ActionImplFromJson(Map<String, dynamic> json) => _$ActionImpl(
method: $enumDecode(_$ActionMethodEnumMap, json['method']),
data: json['data'],
id: json['id'] as String,
);
Map<String, dynamic> _$$ActionImplToJson(_$ActionImpl instance) =>
<String, dynamic>{
'method': _$ActionMethodEnumMap[instance.method]!,
'data': instance.data,
'id': instance.id,
};
const _$ActionMethodEnumMap = {
ActionMethod.message: 'message',
ActionMethod.initClash: 'initClash',
ActionMethod.getIsInit: 'getIsInit',
ActionMethod.forceGc: 'forceGc',
ActionMethod.shutdown: 'shutdown',
ActionMethod.validateConfig: 'validateConfig',
ActionMethod.updateConfig: 'updateConfig',
ActionMethod.getProxies: 'getProxies',
ActionMethod.changeProxy: 'changeProxy',
ActionMethod.getTraffic: 'getTraffic',
ActionMethod.getTotalTraffic: 'getTotalTraffic',
ActionMethod.resetTraffic: 'resetTraffic',
ActionMethod.asyncTestDelay: 'asyncTestDelay',
ActionMethod.getConnections: 'getConnections',
ActionMethod.closeConnections: 'closeConnections',
ActionMethod.closeConnection: 'closeConnection',
ActionMethod.getExternalProviders: 'getExternalProviders',
ActionMethod.getExternalProvider: 'getExternalProvider',
ActionMethod.updateGeoData: 'updateGeoData',
ActionMethod.updateExternalProvider: 'updateExternalProvider',
ActionMethod.sideLoadExternalProvider: 'sideLoadExternalProvider',
ActionMethod.startLog: 'startLog',
ActionMethod.stopLog: 'stopLog',
ActionMethod.startListener: 'startListener',
ActionMethod.stopListener: 'stopListener',
};

View File

@@ -1046,13 +1046,15 @@ abstract class _ApplicationSelectorState implements ApplicationSelectorState {
/// @nodoc
mixin _$TrayState {
Mode get mode => throw _privateConstructorUsedError;
int get port => throw _privateConstructorUsedError;
bool get autoLaunch => throw _privateConstructorUsedError;
bool get adminAutoLaunch => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError;
bool get tunEnable => throw _privateConstructorUsedError;
bool get isStart => throw _privateConstructorUsedError;
String? get locale => throw _privateConstructorUsedError;
Brightness? get brightness => throw _privateConstructorUsedError;
List<Group> get groups => throw _privateConstructorUsedError;
Map<String, String> get map => throw _privateConstructorUsedError;
/// Create a copy of TrayState
/// with the given fields replaced by the non-null parameter values.
@@ -1068,13 +1070,15 @@ abstract class $TrayStateCopyWith<$Res> {
@useResult
$Res call(
{Mode mode,
int port,
bool autoLaunch,
bool adminAutoLaunch,
bool systemProxy,
bool tunEnable,
bool isStart,
String? locale,
Brightness? brightness});
Brightness? brightness,
List<Group> groups,
Map<String, String> map});
}
/// @nodoc
@@ -1093,27 +1097,29 @@ class _$TrayStateCopyWithImpl<$Res, $Val extends TrayState>
@override
$Res call({
Object? mode = null,
Object? port = null,
Object? autoLaunch = null,
Object? adminAutoLaunch = null,
Object? systemProxy = null,
Object? tunEnable = null,
Object? isStart = null,
Object? locale = freezed,
Object? brightness = freezed,
Object? groups = null,
Object? map = null,
}) {
return _then(_value.copyWith(
mode: null == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as Mode,
port: null == port
? _value.port
: port // ignore: cast_nullable_to_non_nullable
as int,
autoLaunch: null == autoLaunch
? _value.autoLaunch
: autoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
adminAutoLaunch: null == adminAutoLaunch
? _value.adminAutoLaunch
: adminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
@@ -1134,6 +1140,14 @@ class _$TrayStateCopyWithImpl<$Res, $Val extends TrayState>
? _value.brightness
: brightness // ignore: cast_nullable_to_non_nullable
as Brightness?,
groups: null == groups
? _value.groups
: groups // ignore: cast_nullable_to_non_nullable
as List<Group>,
map: null == map
? _value.map
: map // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
) as $Val);
}
}
@@ -1148,13 +1162,15 @@ abstract class _$$TrayStateImplCopyWith<$Res>
@useResult
$Res call(
{Mode mode,
int port,
bool autoLaunch,
bool adminAutoLaunch,
bool systemProxy,
bool tunEnable,
bool isStart,
String? locale,
Brightness? brightness});
Brightness? brightness,
List<Group> groups,
Map<String, String> map});
}
/// @nodoc
@@ -1171,27 +1187,29 @@ class __$$TrayStateImplCopyWithImpl<$Res>
@override
$Res call({
Object? mode = null,
Object? port = null,
Object? autoLaunch = null,
Object? adminAutoLaunch = null,
Object? systemProxy = null,
Object? tunEnable = null,
Object? isStart = null,
Object? locale = freezed,
Object? brightness = freezed,
Object? groups = null,
Object? map = null,
}) {
return _then(_$TrayStateImpl(
mode: null == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as Mode,
port: null == port
? _value.port
: port // ignore: cast_nullable_to_non_nullable
as int,
autoLaunch: null == autoLaunch
? _value.autoLaunch
: autoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
adminAutoLaunch: null == adminAutoLaunch
? _value.adminAutoLaunch
: adminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
@@ -1212,6 +1230,14 @@ class __$$TrayStateImplCopyWithImpl<$Res>
? _value.brightness
: brightness // ignore: cast_nullable_to_non_nullable
as Brightness?,
groups: null == groups
? _value._groups
: groups // ignore: cast_nullable_to_non_nullable
as List<Group>,
map: null == map
? _value._map
: map // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
));
}
}
@@ -1221,20 +1247,24 @@ class __$$TrayStateImplCopyWithImpl<$Res>
class _$TrayStateImpl implements _TrayState {
const _$TrayStateImpl(
{required this.mode,
required this.port,
required this.autoLaunch,
required this.adminAutoLaunch,
required this.systemProxy,
required this.tunEnable,
required this.isStart,
required this.locale,
required this.brightness});
required this.brightness,
required final List<Group> groups,
required final Map<String, String> map})
: _groups = groups,
_map = map;
@override
final Mode mode;
@override
final bool autoLaunch;
final int port;
@override
final bool adminAutoLaunch;
final bool autoLaunch;
@override
final bool systemProxy;
@override
@@ -1245,10 +1275,25 @@ class _$TrayStateImpl implements _TrayState {
final String? locale;
@override
final Brightness? brightness;
final List<Group> _groups;
@override
List<Group> get groups {
if (_groups is EqualUnmodifiableListView) return _groups;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_groups);
}
final Map<String, String> _map;
@override
Map<String, String> get map {
if (_map is EqualUnmodifiableMapView) return _map;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_map);
}
@override
String toString() {
return 'TrayState(mode: $mode, autoLaunch: $autoLaunch, adminAutoLaunch: $adminAutoLaunch, systemProxy: $systemProxy, tunEnable: $tunEnable, isStart: $isStart, locale: $locale, brightness: $brightness)';
return 'TrayState(mode: $mode, port: $port, autoLaunch: $autoLaunch, systemProxy: $systemProxy, tunEnable: $tunEnable, isStart: $isStart, locale: $locale, brightness: $brightness, groups: $groups, map: $map)';
}
@override
@@ -1257,10 +1302,9 @@ class _$TrayStateImpl implements _TrayState {
(other.runtimeType == runtimeType &&
other is _$TrayStateImpl &&
(identical(other.mode, mode) || other.mode == mode) &&
(identical(other.port, port) || other.port == port) &&
(identical(other.autoLaunch, autoLaunch) ||
other.autoLaunch == autoLaunch) &&
(identical(other.adminAutoLaunch, adminAutoLaunch) ||
other.adminAutoLaunch == adminAutoLaunch) &&
(identical(other.systemProxy, systemProxy) ||
other.systemProxy == systemProxy) &&
(identical(other.tunEnable, tunEnable) ||
@@ -1268,12 +1312,24 @@ class _$TrayStateImpl implements _TrayState {
(identical(other.isStart, isStart) || other.isStart == isStart) &&
(identical(other.locale, locale) || other.locale == locale) &&
(identical(other.brightness, brightness) ||
other.brightness == brightness));
other.brightness == brightness) &&
const DeepCollectionEquality().equals(other._groups, _groups) &&
const DeepCollectionEquality().equals(other._map, _map));
}
@override
int get hashCode => Object.hash(runtimeType, mode, autoLaunch,
adminAutoLaunch, systemProxy, tunEnable, isStart, locale, brightness);
int get hashCode => Object.hash(
runtimeType,
mode,
port,
autoLaunch,
systemProxy,
tunEnable,
isStart,
locale,
brightness,
const DeepCollectionEquality().hash(_groups),
const DeepCollectionEquality().hash(_map));
/// Create a copy of TrayState
/// with the given fields replaced by the non-null parameter values.
@@ -1287,20 +1343,22 @@ class _$TrayStateImpl implements _TrayState {
abstract class _TrayState implements TrayState {
const factory _TrayState(
{required final Mode mode,
required final int port,
required final bool autoLaunch,
required final bool adminAutoLaunch,
required final bool systemProxy,
required final bool tunEnable,
required final bool isStart,
required final String? locale,
required final Brightness? brightness}) = _$TrayStateImpl;
required final Brightness? brightness,
required final List<Group> groups,
required final Map<String, String> map}) = _$TrayStateImpl;
@override
Mode get mode;
@override
bool get autoLaunch;
int get port;
@override
bool get adminAutoLaunch;
bool get autoLaunch;
@override
bool get systemProxy;
@override
@@ -1311,6 +1369,10 @@ abstract class _TrayState implements TrayState {
String? get locale;
@override
Brightness? get brightness;
@override
List<Group> get groups;
@override
Map<String, String> get map;
/// Create a copy of TrayState
/// with the given fields replaced by the non-null parameter values.
@@ -3150,7 +3212,6 @@ abstract class _ProxiesActionsState implements ProxiesActionsState {
/// @nodoc
mixin _$AutoLaunchState {
bool get isAutoLaunch => throw _privateConstructorUsedError;
bool get isAdminAutoLaunch => throw _privateConstructorUsedError;
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@@ -3165,7 +3226,7 @@ abstract class $AutoLaunchStateCopyWith<$Res> {
AutoLaunchState value, $Res Function(AutoLaunchState) then) =
_$AutoLaunchStateCopyWithImpl<$Res, AutoLaunchState>;
@useResult
$Res call({bool isAutoLaunch, bool isAdminAutoLaunch});
$Res call({bool isAutoLaunch});
}
/// @nodoc
@@ -3184,17 +3245,12 @@ class _$AutoLaunchStateCopyWithImpl<$Res, $Val extends AutoLaunchState>
@override
$Res call({
Object? isAutoLaunch = null,
Object? isAdminAutoLaunch = null,
}) {
return _then(_value.copyWith(
isAutoLaunch: null == isAutoLaunch
? _value.isAutoLaunch
: isAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
isAdminAutoLaunch: null == isAdminAutoLaunch
? _value.isAdminAutoLaunch
: isAdminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
@@ -3207,7 +3263,7 @@ abstract class _$$AutoLaunchStateImplCopyWith<$Res>
__$$AutoLaunchStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isAutoLaunch, bool isAdminAutoLaunch});
$Res call({bool isAutoLaunch});
}
/// @nodoc
@@ -3224,17 +3280,12 @@ class __$$AutoLaunchStateImplCopyWithImpl<$Res>
@override
$Res call({
Object? isAutoLaunch = null,
Object? isAdminAutoLaunch = null,
}) {
return _then(_$AutoLaunchStateImpl(
isAutoLaunch: null == isAutoLaunch
? _value.isAutoLaunch
: isAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
isAdminAutoLaunch: null == isAdminAutoLaunch
? _value.isAdminAutoLaunch
: isAdminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@@ -3242,17 +3293,14 @@ class __$$AutoLaunchStateImplCopyWithImpl<$Res>
/// @nodoc
class _$AutoLaunchStateImpl implements _AutoLaunchState {
const _$AutoLaunchStateImpl(
{required this.isAutoLaunch, required this.isAdminAutoLaunch});
const _$AutoLaunchStateImpl({required this.isAutoLaunch});
@override
final bool isAutoLaunch;
@override
final bool isAdminAutoLaunch;
@override
String toString() {
return 'AutoLaunchState(isAutoLaunch: $isAutoLaunch, isAdminAutoLaunch: $isAdminAutoLaunch)';
return 'AutoLaunchState(isAutoLaunch: $isAutoLaunch)';
}
@override
@@ -3261,13 +3309,11 @@ class _$AutoLaunchStateImpl implements _AutoLaunchState {
(other.runtimeType == runtimeType &&
other is _$AutoLaunchStateImpl &&
(identical(other.isAutoLaunch, isAutoLaunch) ||
other.isAutoLaunch == isAutoLaunch) &&
(identical(other.isAdminAutoLaunch, isAdminAutoLaunch) ||
other.isAdminAutoLaunch == isAdminAutoLaunch));
other.isAutoLaunch == isAutoLaunch));
}
@override
int get hashCode => Object.hash(runtimeType, isAutoLaunch, isAdminAutoLaunch);
int get hashCode => Object.hash(runtimeType, isAutoLaunch);
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@@ -3280,14 +3326,11 @@ class _$AutoLaunchStateImpl implements _AutoLaunchState {
}
abstract class _AutoLaunchState implements AutoLaunchState {
const factory _AutoLaunchState(
{required final bool isAutoLaunch,
required final bool isAdminAutoLaunch}) = _$AutoLaunchStateImpl;
const factory _AutoLaunchState({required final bool isAutoLaunch}) =
_$AutoLaunchStateImpl;
@override
bool get isAutoLaunch;
@override
bool get isAdminAutoLaunch;
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.

View File

@@ -1,7 +1,7 @@
export 'app.dart';
export 'clash_config.dart';
export 'common.dart';
export 'config.dart';
export 'core.dart';
export 'profile.dart';
export 'ffi.dart';
export 'selector.dart';
export 'common.dart';

View File

@@ -96,6 +96,21 @@ extension ProfileExtension on Profile {
return await File(profilePath!).exists();
}
Future<File> getFile() async {
final path = await appPath.getProfilePath(id);
final file = File(path!);
final isExists = await file.exists();
if (!isExists) {
await file.create(recursive: true);
}
return file;
}
Future<int> get profileLastModified async {
final file = await getFile();
return (await file.lastModified()).microsecondsSinceEpoch;
}
Future<Profile> update() async {
final response = await request.getFileResponseForUrl(url);
final disposition = response.headers.value("content-disposition");
@@ -111,12 +126,7 @@ extension ProfileExtension on Profile {
if (message.isNotEmpty) {
throw message;
}
final path = await appPath.getProfilePath(id);
final file = File(path!);
final isExists = await file.exists();
if (!isExists) {
await file.create(recursive: true);
}
final file = await getFile();
await file.writeAsBytes(bytes);
return copyWith(lastUpdateDate: DateTime.now());
}
@@ -126,12 +136,7 @@ extension ProfileExtension on Profile {
if (message.isNotEmpty) {
throw message;
}
final path = await appPath.getProfilePath(id);
final file = File(path!);
final isExists = await file.exists();
if (!isExists) {
await file.create(recursive: true);
}
final file = await getFile();
await file.writeAsString(value);
return copyWith(lastUpdateDate: DateTime.now());
}

View File

@@ -63,13 +63,15 @@ class ApplicationSelectorState with _$ApplicationSelectorState {
class TrayState with _$TrayState {
const factory TrayState({
required Mode mode,
required int port,
required bool autoLaunch,
required bool adminAutoLaunch,
required bool systemProxy,
required bool tunEnable,
required bool isStart,
required String? locale,
required Brightness? brightness,
required List<Group> groups,
required SelectedMap map,
}) = _TrayState;
}
@@ -197,7 +199,6 @@ class ProxiesActionsState with _$ProxiesActionsState {
class AutoLaunchState with _$AutoLaunchState {
const factory AutoLaunchState({
required bool isAutoLaunch,
required bool isAdminAutoLaunch,
}) = _AutoLaunchState;
}

View File

@@ -27,7 +27,7 @@ class Vpn {
clashCore.requestGc();
case "dnsChanged":
final dns = call.arguments as String;
clashCore.updateDns(dns);
clashLib?.updateDns(dns);
default:
throw MissingPluginException();
}
@@ -40,7 +40,7 @@ class Vpn {
}
Future<bool?> startVpn() async {
final options = clashCore.getAndroidVpnOptions();
final options = clashLib?.getAndroidVpnOptions();
return await methodChannel.invokeMethod<bool>("start", {
'data': json.encode(options),
});
@@ -54,7 +54,7 @@ class Vpn {
return await methodChannel.invokeMethod<bool?>("setProtect", {'fd': fd});
}
Future<String?> resolverProcess(Process process) async {
Future<String?> resolverProcess(ProcessData process) async {
return await methodChannel.invokeMethod<String>("resolverProcess", {
"data": json.encode(process),
});
@@ -79,7 +79,7 @@ class Vpn {
receiver!.listen((message) {
_handleServiceMessage(message);
});
clashCore.startTun(fd, receiver!.sendPort.nativePort);
clashLib?.startTun(fd, receiver!.sendPort.nativePort);
}
setServiceMessageHandler(ServiceMessageListener serviceMessageListener) {
@@ -92,7 +92,7 @@ class Vpn {
case ServiceMessageType.protect:
_serviceMessageHandler?.onProtect(Fd.fromJson(m.data));
case ServiceMessageType.process:
_serviceMessageHandler?.onProcess(Process.fromJson(m.data));
_serviceMessageHandler?.onProcess(ProcessData.fromJson(m.data));
case ServiceMessageType.started:
_serviceMessageHandler?.onStarted(m.data);
case ServiceMessageType.loaded:

View File

@@ -8,9 +8,7 @@ import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/plugins/vpn.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart';
@@ -30,6 +28,8 @@ class GlobalState {
late AppController appController;
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
List<Function> updateFunctionLists = [];
bool lastTunEnable = false;
int? lastProfileModified;
bool get isStart => startTime != null && startTime!.isBeforeNow;
@@ -47,16 +47,61 @@ class GlobalState {
timer?.cancel();
}
Future<void> initCore({
required AppState appState,
required ClashConfig clashConfig,
required Config config,
}) async {
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
await applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
Future<void> updateClashConfig({
required AppState appState,
required ClashConfig clashConfig,
required Config config,
bool isPatch = true,
}) async {
await config.currentProfile?.checkAndUpdate();
final useClashConfig = clashConfig.copyWith();
if (clashConfig.tun.enable != lastTunEnable &&
lastTunEnable == false &&
!Platform.isAndroid) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.none:
break;
case AuthorizeCode.success:
lastTunEnable = useClashConfig.tun.enable;
await restartCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
return;
case AuthorizeCode.error:
useClashConfig.tun = useClashConfig.tun.copyWith(
enable: false,
);
}
}
if (config.appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
final res = await clashCore.updateConfig(
UpdateConfigParams(
profileId: config.currentProfileId ?? "",
config: clashConfig,
config: useClashConfig,
params: ConfigExtendedParams(
isPatch: isPatch,
isCompatible: true,
@@ -67,14 +112,12 @@ class GlobalState {
),
);
if (res.isNotEmpty) throw res;
}
updateCoreVersionInfo(AppState appState) {
appState.versionInfo = clashCore.getVersionInfo();
lastTunEnable = useClashConfig.tun.enable;
lastProfileModified = await config.getCurrentProfile()?.profileLastModified;
}
handleStart() async {
clashCore.start();
await clashCore.startListener();
if (globalState.isVpnService) {
await vpn?.startVpn();
startListenUpdate();
@@ -85,17 +128,32 @@ class GlobalState {
startListenUpdate();
}
restartCore({
required AppState appState,
required ClashConfig clashConfig,
required Config config,
bool isPatch = true,
}) async {
await clashService?.startCore();
await initCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
if (isStart) {
await handleStart();
}
}
updateStartTime() {
startTime = clashCore.getRunTime();
startTime = clashLib?.getRunTime();
}
Future handleStop() async {
clashCore.stop();
if (Platform.isAndroid) {
clashCore.stopTun();
}
await service?.destroy();
startTime = null;
await clashCore.stopListener();
clashLib?.stopTun();
await service?.destroy();
stopListenUpdate();
}
@@ -106,6 +164,7 @@ class GlobalState {
}) async {
clashCore.requestGc();
await updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: false,
@@ -123,30 +182,27 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) async {
appState.isInit = clashCore.isInit;
appState.isInit = await clashCore.isInit;
if (!appState.isInit) {
appState.isInit = await clashService.init(
appState.isInit = await clashCore.init(
config: config,
clashConfig: clashConfig,
);
if (Platform.isAndroid) {
clashCore.setState(
CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
bypassDomain: config.networkProps.bypassDomain,
routeAddress: clashConfig.routeAddress,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
),
);
}
clashLib?.setState(
CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
bypassDomain: config.networkProps.bypassDomain,
routeAddress: clashConfig.routeAddress,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
),
);
}
updateCoreVersionInfo(appState);
}
Future<void> updateGroups(AppState appState) async {
@@ -198,8 +254,8 @@ class GlobalState {
required Config config,
required String groupName,
required String proxyName,
}) {
clashCore.changeProxy(
}) async {
await clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
@@ -226,18 +282,21 @@ class GlobalState {
}
updateTraffic({
required Config config,
AppFlowingState? appFlowingState,
}) {
final traffic = clashCore.getTraffic();
}) async {
final onlyProxy = config.appSetting.onlyProxy;
final traffic = await clashCore.getTraffic(onlyProxy);
if (Platform.isAndroid && isVpnService == true) {
vpn?.startForeground(
title: clashCore.getCurrentProfileName(),
title: clashLib?.getCurrentProfileName() ?? "",
content: "$traffic",
);
} else {
if (appFlowingState != null) {
appFlowingState.addTraffic(traffic);
appFlowingState.totalTraffic = clashCore.getTotalTraffic();
appFlowingState.totalTraffic =
await clashCore.getTotalTraffic(onlyProxy);
}
}
}
@@ -301,132 +360,6 @@ class GlobalState {
},
);
}
Future _updateSystemTray({
required Brightness? brightness,
bool force = false,
}) async {
if (Platform.isAndroid) {
return;
}
if (Platform.isLinux || force) {
await trayManager.destroy();
}
await trayManager.setIcon(
other.getTrayIconPath(
brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness,
),
isTemplate: true,
);
if (!Platform.isLinux) {
await trayManager.setToolTip(
appName,
);
}
}
updateTray({
required AppState appState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
bool focus = false,
}) async {
if (!Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
force: focus,
);
}
List<MenuItem> menuItems = [];
final showMenuItem = MenuItem(
label: appLocalizations.show,
onClick: (_) {
window?.show();
},
);
menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async {
globalState.appController.updateStart();
},
checked: false,
);
menuItems.add(startMenuItem);
menuItems.add(MenuItem.separator());
for (final mode in Mode.values) {
menuItems.add(
MenuItem.checkbox(
label: Intl.message(mode.name),
onClick: (_) {
globalState.appController.clashConfig.mode = mode;
},
checked: mode == clashConfig.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (appFlowingState.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
globalState.appController.updateTun();
},
checked: clashConfig.tun.enable,
),
);
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.systemProxy,
onClick: (_) {
globalState.appController.updateSystemProxy();
},
checked: config.networkProps.systemProxy,
),
);
menuItems.add(MenuItem.separator());
}
final autoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.autoLaunch,
onClick: (_) async {
globalState.appController.updateAutoLaunch();
},
checked: config.appSetting.autoLaunch,
);
menuItems.add(autoStartMenuItem);
if (Platform.isWindows) {
final adminAutoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.adminAutoLaunch,
onClick: (_) async {
globalState.appController.updateAdminAutoLaunch();
},
checked: config.appSetting.adminAutoLaunch,
);
menuItems.add(adminAutoStartMenuItem);
}
menuItems.add(MenuItem.separator());
final exitMenuItem = MenuItem(
label: appLocalizations.exit,
onClick: (_) async {
await globalState.appController.handleExit();
},
);
menuItems.add(exitMenuItem);
final menu = Menu(items: menuItems);
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
force: focus,
);
}
}
}
final globalState = GlobalState();

View File

@@ -135,9 +135,6 @@ class CommonCard extends StatelessWidget {
if (isSelected) {
return colorScheme.secondaryContainer;
}
if (states.isEmpty) {
return colorScheme.surfaceContainerLow;
}
return colorScheme.surfaceContainer;
}
}

View File

@@ -59,7 +59,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
set floatingActionButton(Widget floatingActionButton) {
set floatingActionButton(Widget? floatingActionButton) {
if (_floatingActionButton.value != floatingActionButton) {
_floatingActionButton.value = floatingActionButton;
}
@@ -122,25 +122,27 @@ class CommonScaffoldState extends State<CommonScaffold> {
valueListenable: _actions,
builder: (_, actions, __) {
final realActions =
actions.isNotEmpty ? actions : widget.actions;
actions.isNotEmpty ? actions : widget.actions;
return AppBar(
centerTitle: false,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: widget.bottomNavigationBar != null
? context.colorScheme.surfaceContainer
: context.colorScheme.surface,
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor:
widget.bottomNavigationBar != null
? context.colorScheme.surfaceContainer
: context.colorScheme.surface,
systemNavigationBarDividerColor: Colors.transparent,
),
automaticallyImplyLeading: widget.automaticallyImplyLeading,
automaticallyImplyLeading:
widget.automaticallyImplyLeading,
leading: widget.leading,
title: Text(widget.title),
actions: [