Update popup menu
Add file editor Fix android service issues Optimize desktop background performance Optimize android main process performance Optimize delay test Optimize vpn protect
This commit is contained in:
@@ -153,7 +153,10 @@ class ApplicationState extends State<Application> {
|
||||
return AppStateManager(
|
||||
child: ClashManager(
|
||||
child: ConnectivityManager(
|
||||
onConnectivityChanged: globalState.appController.updateLocalIp,
|
||||
onConnectivityChanged: () {
|
||||
globalState.appController.updateLocalIp();
|
||||
globalState.appController.addCheckIpNumDebounce();
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
@@ -175,8 +178,8 @@ class ApplicationState extends State<Application> {
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildWrap(
|
||||
_buildPlatformWrap(
|
||||
return _buildPlatformWrap(
|
||||
_buildWrap(
|
||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.appSetting.locale,
|
||||
@@ -252,7 +255,7 @@ class ApplicationState extends State<Application> {
|
||||
linkManager.destroy();
|
||||
_autoUpdateGroupTaskTimer?.cancel();
|
||||
_autoUpdateProfilesTaskTimer?.cancel();
|
||||
await clashService?.destroy();
|
||||
await clashCore.destroy();
|
||||
await globalState.appController.savePreferences();
|
||||
await globalState.appController.handleExit();
|
||||
super.dispose();
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:path/path.dart';
|
||||
|
||||
class ClashCore {
|
||||
static ClashCore? _instance;
|
||||
late ClashInterface clashInterface;
|
||||
late ClashHandlerInterface clashInterface;
|
||||
|
||||
ClashCore._internal() {
|
||||
if (Platform.isAndroid) {
|
||||
@@ -28,7 +28,11 @@ class ClashCore {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> _initGeo() async {
|
||||
Future<bool> preload() {
|
||||
return clashInterface.preload();
|
||||
}
|
||||
|
||||
static Future<void> initGeo() async {
|
||||
final homePath = await appPath.getHomeDirPath();
|
||||
final homeDir = Directory(homePath);
|
||||
final isExists = await homeDir.exists();
|
||||
@@ -63,7 +67,7 @@ class ClashCore {
|
||||
required ClashConfig clashConfig,
|
||||
required Config config,
|
||||
}) async {
|
||||
await _initGeo();
|
||||
await initGeo();
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
return await clashInterface.init(homeDirPath);
|
||||
}
|
||||
@@ -135,6 +139,9 @@ class ClashCore {
|
||||
Future<List<ExternalProvider>> getExternalProviders() async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProviders();
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return Isolate.run<List<ExternalProvider>>(
|
||||
() {
|
||||
final externalProviders =
|
||||
@@ -152,7 +159,7 @@ class ClashCore {
|
||||
String externalProviderName) async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProvider(externalProviderName);
|
||||
if (externalProvidersRawString == null) {
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
@@ -161,11 +168,8 @@ class ClashCore {
|
||||
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
|
||||
}
|
||||
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
}) {
|
||||
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||
return clashInterface.updateGeoData(params);
|
||||
}
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
@@ -190,13 +194,16 @@ class ClashCore {
|
||||
await clashInterface.stopListener();
|
||||
}
|
||||
|
||||
Future<Delay> getDelay(String proxyName) async {
|
||||
final data = await clashInterface.asyncTestDelay(proxyName);
|
||||
Future<Delay> getDelay(String url, String proxyName) async {
|
||||
final data = await clashInterface.asyncTestDelay(url, proxyName);
|
||||
return Delay.fromJson(json.decode(data));
|
||||
}
|
||||
|
||||
Future<Traffic> getTraffic(bool value) async {
|
||||
final trafficString = await clashInterface.getTraffic(value);
|
||||
Future<Traffic> getTraffic() async {
|
||||
final trafficString = await clashInterface.getTraffic();
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
@@ -211,13 +218,19 @@ class ClashCore {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Traffic> getTotalTraffic(bool value) async {
|
||||
final totalTrafficString = await clashInterface.getTotalTraffic(value);
|
||||
Future<Traffic> getTotalTraffic() async {
|
||||
final totalTrafficString = await clashInterface.getTotalTraffic();
|
||||
if (totalTrafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(totalTrafficString));
|
||||
}
|
||||
|
||||
Future<int> getMemory() async {
|
||||
final value = await clashInterface.getMemory();
|
||||
if (value.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
return int.parse(value);
|
||||
}
|
||||
|
||||
@@ -236,6 +249,10 @@ class ClashCore {
|
||||
requestGc() {
|
||||
clashInterface.forceGc();
|
||||
}
|
||||
|
||||
destroy() async {
|
||||
await clashInterface.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
final clashCore = ClashCore();
|
||||
|
||||
@@ -2362,18 +2362,39 @@ class ClashFFI {
|
||||
late final _initNativeApiBridge = _initNativeApiBridgePtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void initMessage(
|
||||
int port,
|
||||
void attachMessagePort(
|
||||
int mPort,
|
||||
) {
|
||||
return _initMessage(
|
||||
port,
|
||||
return _attachMessagePort(
|
||||
mPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _initMessagePtr =
|
||||
late final _attachMessagePortPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'initMessage');
|
||||
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
|
||||
'attachMessagePort');
|
||||
late final _attachMessagePort =
|
||||
_attachMessagePortPtr.asFunction<void Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTraffic() {
|
||||
return _getTraffic();
|
||||
}
|
||||
|
||||
late final _getTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getTraffic');
|
||||
late final _getTraffic =
|
||||
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTotalTraffic() {
|
||||
return _getTotalTraffic();
|
||||
}
|
||||
|
||||
late final _getTotalTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getTotalTraffic');
|
||||
late final _getTotalTraffic =
|
||||
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void freeCString(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
@@ -2389,19 +2410,22 @@ class ClashFFI {
|
||||
late final _freeCString =
|
||||
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
int initClash(
|
||||
ffi.Pointer<ffi.Char> homeDirStr,
|
||||
void invokeAction(
|
||||
ffi.Pointer<ffi.Char> paramsChar,
|
||||
int port,
|
||||
) {
|
||||
return _initClash(
|
||||
homeDirStr,
|
||||
return _invokeAction(
|
||||
paramsChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _initClashPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'initClash');
|
||||
late final _initClash =
|
||||
_initClashPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
|
||||
late final _invokeActionPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('invokeAction');
|
||||
late final _invokeAction =
|
||||
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void startListener() {
|
||||
return _startListener();
|
||||
@@ -2419,317 +2443,55 @@ class ClashFFI {
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
|
||||
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
|
||||
|
||||
int getIsInit() {
|
||||
return _getIsInit();
|
||||
void attachInvokePort(
|
||||
int mPort,
|
||||
) {
|
||||
return _attachInvokePort(
|
||||
mPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getIsInitPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('getIsInit');
|
||||
late final _getIsInit = _getIsInitPtr.asFunction<int Function()>();
|
||||
late final _attachInvokePortPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'attachInvokePort');
|
||||
late final _attachInvokePort =
|
||||
_attachInvokePortPtr.asFunction<void Function(int)>();
|
||||
|
||||
int shutdownClash() {
|
||||
return _shutdownClash();
|
||||
}
|
||||
|
||||
late final _shutdownClashPtr =
|
||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('shutdownClash');
|
||||
late final _shutdownClash = _shutdownClashPtr.asFunction<int Function()>();
|
||||
|
||||
void forceGc() {
|
||||
return _forceGc();
|
||||
}
|
||||
|
||||
late final _forceGcPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
|
||||
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
|
||||
|
||||
void validateConfig(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
void quickStart(
|
||||
ffi.Pointer<ffi.Char> dirChar,
|
||||
ffi.Pointer<ffi.Char> paramsChar,
|
||||
ffi.Pointer<ffi.Char> stateParamsChar,
|
||||
int port,
|
||||
) {
|
||||
return _validateConfig(
|
||||
s,
|
||||
return _quickStart(
|
||||
dirChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _validateConfigPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('validateConfig');
|
||||
late final _validateConfig = _validateConfigPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void updateConfig(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _updateConfig(
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateConfigPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateConfig');
|
||||
late final _updateConfig =
|
||||
_updateConfigPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getProxies() {
|
||||
return _getProxies();
|
||||
}
|
||||
|
||||
late final _getProxiesPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getProxies');
|
||||
late final _getProxies =
|
||||
_getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
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>, ffi.LongLong)>>('changeProxy');
|
||||
late final _changeProxy =
|
||||
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTraffic(
|
||||
int port,
|
||||
) {
|
||||
return _getTraffic(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'getTraffic');
|
||||
late final _getTraffic =
|
||||
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getTotalTraffic(
|
||||
int port,
|
||||
) {
|
||||
return _getTotalTraffic(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getTotalTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'getTotalTraffic');
|
||||
late final _getTotalTraffic =
|
||||
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
void resetTraffic() {
|
||||
return _resetTraffic();
|
||||
}
|
||||
|
||||
late final _resetTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('resetTraffic');
|
||||
late final _resetTraffic = _resetTrafficPtr.asFunction<void Function()>();
|
||||
|
||||
void asyncTestDelay(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
int port,
|
||||
) {
|
||||
return _asyncTestDelay(
|
||||
s,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _asyncTestDelayPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('asyncTestDelay');
|
||||
late final _asyncTestDelay = _asyncTestDelayPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getConnections() {
|
||||
return _getConnections();
|
||||
}
|
||||
|
||||
late final _getConnectionsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getConnections');
|
||||
late final _getConnections =
|
||||
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void getMemory(
|
||||
int port,
|
||||
) {
|
||||
return _getMemory(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getMemoryPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('getMemory');
|
||||
late final _getMemory = _getMemoryPtr.asFunction<void Function(int)>();
|
||||
|
||||
void closeConnections() {
|
||||
return _closeConnections();
|
||||
}
|
||||
|
||||
late final _closeConnectionsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('closeConnections');
|
||||
late final _closeConnections =
|
||||
_closeConnectionsPtr.asFunction<void Function()>();
|
||||
|
||||
void closeConnection(
|
||||
ffi.Pointer<ffi.Char> id,
|
||||
) {
|
||||
return _closeConnection(
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
late final _closeConnectionPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'closeConnection');
|
||||
late final _closeConnection =
|
||||
_closeConnectionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getExternalProviders() {
|
||||
return _getExternalProviders();
|
||||
}
|
||||
|
||||
late final _getExternalProvidersPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||
'getExternalProviders');
|
||||
late final _getExternalProviders =
|
||||
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getExternalProvider(
|
||||
ffi.Pointer<ffi.Char> externalProviderNameChar,
|
||||
) {
|
||||
return _getExternalProvider(
|
||||
externalProviderNameChar,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getExternalProviderPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Char>)>>('getExternalProvider');
|
||||
late final _getExternalProvider = _getExternalProviderPtr
|
||||
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void updateGeoData(
|
||||
ffi.Pointer<ffi.Char> geoTypeChar,
|
||||
ffi.Pointer<ffi.Char> geoNameChar,
|
||||
int port,
|
||||
) {
|
||||
return _updateGeoData(
|
||||
geoTypeChar,
|
||||
geoNameChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateGeoDataPtr = _lookup<
|
||||
late final _quickStartPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.LongLong)>>('updateGeoData');
|
||||
late final _updateGeoData = _updateGeoDataPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('quickStart');
|
||||
late final _quickStart = _quickStartPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void updateExternalProvider(
|
||||
ffi.Pointer<ffi.Char> providerNameChar,
|
||||
int port,
|
||||
) {
|
||||
return _updateExternalProvider(
|
||||
providerNameChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateExternalProviderPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateExternalProvider');
|
||||
late final _updateExternalProvider = _updateExternalProviderPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void getCountryCode(
|
||||
ffi.Pointer<ffi.Char> ipChar,
|
||||
int port,
|
||||
) {
|
||||
return _getCountryCode(
|
||||
ipChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getCountryCodePtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('getCountryCode');
|
||||
late final _getCountryCode = _getCountryCodePtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void sideLoadExternalProvider(
|
||||
ffi.Pointer<ffi.Char> providerNameChar,
|
||||
ffi.Pointer<ffi.Char> dataChar,
|
||||
int port,
|
||||
) {
|
||||
return _sideLoadExternalProvider(
|
||||
providerNameChar,
|
||||
dataChar,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _sideLoadExternalProviderPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.LongLong)>>('sideLoadExternalProvider');
|
||||
late final _sideLoadExternalProvider =
|
||||
_sideLoadExternalProviderPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void startLog() {
|
||||
return _startLog();
|
||||
}
|
||||
|
||||
late final _startLogPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('startLog');
|
||||
late final _startLog = _startLogPtr.asFunction<void Function()>();
|
||||
|
||||
void stopLog() {
|
||||
return _stopLog();
|
||||
}
|
||||
|
||||
late final _stopLogPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
|
||||
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
|
||||
|
||||
void startTUN(
|
||||
ffi.Pointer<ffi.Char> startTUN(
|
||||
int fd,
|
||||
int port,
|
||||
) {
|
||||
return _startTUN(
|
||||
fd,
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _startTUNPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||
'startTUN');
|
||||
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
|
||||
late final _startTUN =
|
||||
_startTUNPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getRunTime() {
|
||||
return _getRunTime();
|
||||
@@ -2750,30 +2512,18 @@ class ClashFFI {
|
||||
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
|
||||
|
||||
void setFdMap(
|
||||
int fd,
|
||||
ffi.Pointer<ffi.Char> fdIdChar,
|
||||
) {
|
||||
return _setFdMap(
|
||||
fd,
|
||||
fdIdChar,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
return _setProcessMap(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setProcessMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setProcessMap');
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
'setFdMap');
|
||||
late final _setFdMap =
|
||||
_setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getCurrentProfileName() {
|
||||
return _getCurrentProfileName();
|
||||
@@ -2822,6 +2572,20 @@ class ClashFFI {
|
||||
'updateDns');
|
||||
late final _updateDns =
|
||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void setProcessMap(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _setProcessMap(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setProcessMapPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'setProcessMap');
|
||||
late final _setProcessMap =
|
||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
}
|
||||
|
||||
final class __mbstate_t extends ffi.Union {
|
||||
@@ -3994,8 +3758,6 @@ final class GoSlice extends ffi.Struct {
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
typedef DartGoInt64 = int;
|
||||
typedef GoUint8 = ffi.UnsignedChar;
|
||||
typedef DartGoUint8 = int;
|
||||
|
||||
const int __has_safe_buffers = 1;
|
||||
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/clash/message.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/common/future.dart';
|
||||
import 'package:fl_clash/common/other.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
|
||||
mixin ClashInterface {
|
||||
FutureOr<bool> init(String homeDir);
|
||||
Future<bool> init(String homeDir);
|
||||
|
||||
FutureOr<void> shutdown();
|
||||
Future<bool> preload();
|
||||
|
||||
FutureOr<bool> get isInit;
|
||||
Future<bool> shutdown();
|
||||
|
||||
forceGc();
|
||||
Future<bool> get isInit;
|
||||
|
||||
Future<bool> forceGc();
|
||||
|
||||
FutureOr<String> validateConfig(String data);
|
||||
|
||||
Future<String> asyncTestDelay(String proxyName);
|
||||
Future<String> asyncTestDelay(String url, String proxyName);
|
||||
|
||||
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
|
||||
|
||||
@@ -29,10 +38,7 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<String>? getExternalProvider(String externalProviderName);
|
||||
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
});
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params);
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
@@ -41,9 +47,9 @@ mixin ClashInterface {
|
||||
|
||||
Future<String> updateExternalProvider(String providerName);
|
||||
|
||||
FutureOr<String> getTraffic(bool value);
|
||||
FutureOr<String> getTraffic();
|
||||
|
||||
FutureOr<String> getTotalTraffic(bool value);
|
||||
FutureOr<String> getTotalTraffic();
|
||||
|
||||
FutureOr<String> getCountryCode(String ip);
|
||||
|
||||
@@ -61,3 +67,338 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<bool> closeConnections();
|
||||
}
|
||||
|
||||
mixin AndroidClashInterface {
|
||||
Future<bool> setFdMap(int fd);
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
|
||||
Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
|
||||
Future<DateTime?> startTun(int fd);
|
||||
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||
|
||||
Future<String> getCurrentProfileName();
|
||||
|
||||
Future<DateTime?> getRunTime();
|
||||
}
|
||||
|
||||
abstract class ClashHandlerInterface with ClashInterface {
|
||||
Map<String, Completer> callbackCompleterMap = {};
|
||||
|
||||
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
|
||||
Future.value(false);
|
||||
|
||||
handleResult(ActionResult result) async {
|
||||
final completer = callbackCompleterMap[result.id];
|
||||
try {
|
||||
switch (result.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(result.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:
|
||||
case ActionMethod.getCountryCode:
|
||||
case ActionMethod.getMemory:
|
||||
completer?.complete(result.data as String);
|
||||
return;
|
||||
case ActionMethod.message:
|
||||
clashMessage.controller.add(result.data as String);
|
||||
completer?.complete(true);
|
||||
return;
|
||||
default:
|
||||
final isHandled = await nextHandleResult(result, completer);
|
||||
if (isHandled) {
|
||||
return;
|
||||
}
|
||||
completer?.complete(result.data);
|
||||
}
|
||||
} catch (_) {
|
||||
debugPrint(result.id);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(String message);
|
||||
|
||||
reStart();
|
||||
|
||||
FutureOr<bool> destroy();
|
||||
|
||||
Future<T> invoke<T>({
|
||||
required ActionMethod method,
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) async {
|
||||
final id = "${method.name}#${other.id}";
|
||||
|
||||
callbackCompleterMap[id] = Completer<T>();
|
||||
|
||||
dynamic defaultValue;
|
||||
|
||||
if (T == String) {
|
||||
defaultValue = "";
|
||||
}
|
||||
if (T == bool) {
|
||||
defaultValue = false;
|
||||
}
|
||||
|
||||
sendMessage(
|
||||
json.encode(
|
||||
Action(
|
||||
id: id,
|
||||
method: method,
|
||||
data: data,
|
||||
defaultValue: defaultValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
|
||||
timeout: timeout,
|
||||
onLast: () {
|
||||
callbackCompleterMap.remove(id);
|
||||
},
|
||||
onTimeout: onTimeout ??
|
||||
() {
|
||||
return defaultValue;
|
||||
},
|
||||
functionName: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> init(String homeDir) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.initClash,
|
||||
data: homeDir,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
return await invoke<bool>(
|
||||
method: ActionMethod.shutdown,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get isInit {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.getIsInit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> forceGc() {
|
||||
return invoke<bool>(
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
@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(UpdateGeoDataParams params) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.updateGeoData,
|
||||
data: json.encode(params),
|
||||
);
|
||||
}
|
||||
|
||||
@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() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getTotalTraffic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTraffic() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getTraffic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
resetTraffic() {
|
||||
invoke(method: ActionMethod.resetTraffic);
|
||||
}
|
||||
|
||||
@override
|
||||
startLog() {
|
||||
invoke(method: ActionMethod.startLog);
|
||||
}
|
||||
|
||||
@override
|
||||
stopLog() {
|
||||
invoke<bool>(
|
||||
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 url, String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
"test-url": url,
|
||||
};
|
||||
return invoke<String>(
|
||||
method: ActionMethod.asyncTestDelay,
|
||||
data: json.encode(delayParams),
|
||||
timeout: Duration(
|
||||
milliseconds: 6000,
|
||||
),
|
||||
onTimeout: () {
|
||||
return json.encode(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: -1,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getCountryCode(String ip) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getCountryCode,
|
||||
data: ip,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getMemory() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getMemory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,58 @@ import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'generated/clash_ffi.dart';
|
||||
import 'interface.dart';
|
||||
|
||||
class ClashLib with ClashInterface {
|
||||
class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
static ClashLib? _instance;
|
||||
final receiver = ReceivePort();
|
||||
|
||||
late final ClashFFI clashFFI;
|
||||
|
||||
late final DynamicLibrary lib;
|
||||
Completer<bool> _canSendCompleter = Completer();
|
||||
SendPort? sendPort;
|
||||
final receiverPort = ReceivePort();
|
||||
|
||||
ClashLib._internal() {
|
||||
lib = DynamicLibrary.open("libclash.so");
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
);
|
||||
_initService();
|
||||
}
|
||||
|
||||
@override
|
||||
preload() {
|
||||
return _canSendCompleter.future;
|
||||
}
|
||||
|
||||
_initService() async {
|
||||
await service?.destroy();
|
||||
_registerMainPort(receiverPort.sendPort);
|
||||
receiverPort.listen((message) {
|
||||
if (message is SendPort) {
|
||||
if (_canSendCompleter.isCompleted) {
|
||||
sendPort = null;
|
||||
_canSendCompleter = Completer();
|
||||
}
|
||||
sendPort = message;
|
||||
_canSendCompleter.complete(true);
|
||||
} else {
|
||||
handleResult(
|
||||
ActionResult.fromJson(json.decode(
|
||||
message,
|
||||
)),
|
||||
);
|
||||
}
|
||||
});
|
||||
await service?.init();
|
||||
}
|
||||
|
||||
_registerMainPort(SendPort sendPort) {
|
||||
IsolateNameServer.removePortNameMapping(mainIsolate);
|
||||
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
|
||||
}
|
||||
|
||||
factory ClashLib() {
|
||||
@@ -32,227 +62,154 @@ class ClashLib with ClashInterface {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
initMessage() {
|
||||
clashFFI.initMessage(
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
@override
|
||||
Future<bool> nextHandleResult(result, completer) async {
|
||||
switch (result.method) {
|
||||
case ActionMethod.setFdMap:
|
||||
case ActionMethod.setProcessMap:
|
||||
case ActionMethod.setState:
|
||||
case ActionMethod.stopTun:
|
||||
case ActionMethod.updateDns:
|
||||
completer?.complete(result.data as bool);
|
||||
return true;
|
||||
case ActionMethod.getRunTime:
|
||||
case ActionMethod.startTun:
|
||||
case ActionMethod.getAndroidVpnOptions:
|
||||
case ActionMethod.getCurrentProfileName:
|
||||
completer?.complete(result.data as String);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
destroy() async {
|
||||
await service?.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
closeConnections() {
|
||||
clashFFI.closeConnections();
|
||||
reStart() {
|
||||
_initService();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> shutdown() async {
|
||||
await super.shutdown();
|
||||
destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
startListener() async {
|
||||
clashFFI.startListener();
|
||||
return true;
|
||||
sendMessage(String message) async {
|
||||
await _canSendCompleter.future;
|
||||
sendPort?.send(message);
|
||||
}
|
||||
|
||||
@override
|
||||
stopListener() async {
|
||||
clashFFI.stopListener();
|
||||
return true;
|
||||
Future<bool> setFdMap(int fd) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setFdMap,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> asyncTestDelay(String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
};
|
||||
Future<bool> setProcessMap(item) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setProcessMap,
|
||||
data: item,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setState(CoreState state) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setState,
|
||||
data: json.encode(state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> startTun(int fd) async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.startTun,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopTun() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopTun,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.getAndroidVpnOptions,
|
||||
);
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return AndroidVpnOptions.fromJson(json.decode(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> updateDns(String value) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.updateDns,
|
||||
data: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> getRunTime() async {
|
||||
final runTimeString = await invoke<String>(
|
||||
method: ActionMethod.getRunTime,
|
||||
);
|
||||
if (runTimeString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getCurrentProfileName() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getCurrentProfileName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClashLibHandler {
|
||||
static ClashLibHandler? _instance;
|
||||
|
||||
late final ClashFFI clashFFI;
|
||||
|
||||
late final DynamicLibrary lib;
|
||||
|
||||
ClashLibHandler._internal() {
|
||||
lib = DynamicLibrary.open("libclash.so");
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
);
|
||||
}
|
||||
|
||||
factory ClashLibHandler() {
|
||||
_instance ??= ClashLibHandler._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<String> invokeAction(String actionParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
@@ -261,89 +218,33 @@ class ClashLib with ClashInterface {
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final delayParamsChar =
|
||||
json.encode(delayParams).toNativeUtf8().cast<Char>();
|
||||
clashFFI.asyncTestDelay(
|
||||
delayParamsChar,
|
||||
final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.invokeAction(
|
||||
actionParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(delayParamsChar);
|
||||
malloc.free(actionParamsChar);
|
||||
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();
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getCountryCode(String ip) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final ipChar = ip.toNativeUtf8().cast<Char>();
|
||||
clashFFI.getCountryCode(
|
||||
ipChar,
|
||||
receiver.sendPort.nativePort,
|
||||
attachMessagePort(int messagePort) {
|
||||
clashFFI.attachMessagePort(
|
||||
messagePort,
|
||||
);
|
||||
malloc.free(ipChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getMemory() {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
clashFFI.getMemory(receiver.sendPort.nativePort);
|
||||
return completer.future;
|
||||
attachInvokePort(int invokePort) {
|
||||
clashFFI.attachInvokePort(
|
||||
invokePort,
|
||||
);
|
||||
}
|
||||
|
||||
/// Android
|
||||
|
||||
startTun(int fd, int port) {
|
||||
if (!Platform.isAndroid) return;
|
||||
clashFFI.startTUN(fd, port);
|
||||
DateTime? startTun(int fd) {
|
||||
final runTimeRaw = clashFFI.startTUN(fd);
|
||||
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(runTimeRaw);
|
||||
if (runTimeString.isEmpty) return null;
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
stopTun() {
|
||||
@@ -351,7 +252,6 @@ class ClashLib with ClashInterface {
|
||||
}
|
||||
|
||||
updateDns(String dns) {
|
||||
if (!Platform.isAndroid) return;
|
||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateDns(dnsChar);
|
||||
malloc.free(dnsChar);
|
||||
@@ -384,8 +284,70 @@ class ClashLib with ClashInterface {
|
||||
return AndroidVpnOptions.fromJson(vpnOptions);
|
||||
}
|
||||
|
||||
setFdMap(int fd) {
|
||||
clashFFI.setFdMap(fd);
|
||||
Traffic getTraffic() {
|
||||
final trafficRaw = clashFFI.getTraffic();
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
Traffic getTotalTraffic(bool value) {
|
||||
final trafficRaw = clashFFI.getTotalTraffic();
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
startListener() async {
|
||||
clashFFI.startListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
stopListener() async {
|
||||
clashFFI.stopListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
setFdMap(String id) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.setFdMap(idChar);
|
||||
malloc.free(idChar);
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
String homeDir,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final stateParams = json.encode(state);
|
||||
final homeChar = homeDir.toNativeUtf8().cast<Char>();
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.quickStart(
|
||||
homeChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(homeChar);
|
||||
malloc.free(paramsChar);
|
||||
malloc.free(stateParamsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
DateTime? getRunTime() {
|
||||
@@ -397,4 +359,5 @@ class ClashLib with ClashInterface {
|
||||
}
|
||||
}
|
||||
|
||||
final clashLib = Platform.isAndroid ? ClashLib() : null;
|
||||
ClashLib? get clashLib =>
|
||||
Platform.isAndroid && !globalState.isService ? ClashLib() : null;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
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';
|
||||
|
||||
class ClashMessage {
|
||||
final controller = StreamController();
|
||||
final controller = StreamController<String>();
|
||||
|
||||
ClashMessage._() {
|
||||
clashLib?.receiver.listen(controller.add);
|
||||
controller.stream.listen(
|
||||
(message) {
|
||||
if(message.isEmpty){
|
||||
return;
|
||||
}
|
||||
final m = AppMessage.fromJson(json.decode(message));
|
||||
for (final AppMessageListener listener in _listeners) {
|
||||
switch (m.type) {
|
||||
@@ -25,9 +26,6 @@ class ClashMessage {
|
||||
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;
|
||||
|
||||
@@ -3,21 +3,17 @@ 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/enum/enum.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
|
||||
class ClashService with ClashInterface {
|
||||
class ClashService extends ClashHandlerInterface {
|
||||
static ClashService? _instance;
|
||||
|
||||
Completer<ServerSocket> serverCompleter = Completer();
|
||||
|
||||
Completer<Socket> socketCompleter = Completer();
|
||||
|
||||
Map<String, Completer> callbackCompleterMap = {};
|
||||
|
||||
Process? process;
|
||||
|
||||
factory ClashService() {
|
||||
@@ -26,11 +22,11 @@ class ClashService with ClashInterface {
|
||||
}
|
||||
|
||||
ClashService._internal() {
|
||||
_createServer();
|
||||
startCore();
|
||||
_initServer();
|
||||
reStart();
|
||||
}
|
||||
|
||||
_createServer() async {
|
||||
_initServer() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
@@ -61,8 +57,8 @@ class ClashService with ClashInterface {
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
_handleAction(
|
||||
Action.fromJson(
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
@@ -71,7 +67,8 @@ class ClashService with ClashInterface {
|
||||
}
|
||||
}
|
||||
|
||||
startCore() async {
|
||||
@override
|
||||
reStart() async {
|
||||
if (process != null) {
|
||||
await shutdown();
|
||||
}
|
||||
@@ -95,6 +92,20 @@ class ClashService with ClashInterface {
|
||||
process!.stdout.listen((_) {});
|
||||
}
|
||||
|
||||
@override
|
||||
destroy() async {
|
||||
final server = await serverCompleter.future;
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
sendMessage(String message) async {
|
||||
final socket = await socketCompleter.future;
|
||||
socket.writeln(message);
|
||||
}
|
||||
|
||||
_deleteSocketFile() async {
|
||||
if (!Platform.isWindows) {
|
||||
final file = File(unixSocketPath);
|
||||
@@ -112,327 +123,22 @@ class ClashService with ClashInterface {
|
||||
}
|
||||
}
|
||||
|
||||
_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:
|
||||
case ActionMethod.getCountryCode:
|
||||
case ActionMethod.getMemory:
|
||||
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:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> _invoke<T>({
|
||||
required ActionMethod method,
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) async {
|
||||
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 ??
|
||||
() {
|
||||
if (T is String) {
|
||||
return "" as T;
|
||||
}
|
||||
if (T is bool) {
|
||||
return false as T;
|
||||
}
|
||||
return null as T;
|
||||
},
|
||||
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,
|
||||
);
|
||||
await super.shutdown();
|
||||
if (Platform.isWindows) {
|
||||
await request.stopCoreByHelper();
|
||||
}
|
||||
await _destroySocket();
|
||||
process?.kill();
|
||||
process = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getCountryCode(String ip) {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getCountryCode,
|
||||
data: ip,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getMemory() {
|
||||
return _invoke<String>(
|
||||
method: ActionMethod.getMemory,
|
||||
);
|
||||
Future<bool> preload() async {
|
||||
await serverCompleter.future;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,3 +34,4 @@ export 'text.dart';
|
||||
export 'tray.dart';
|
||||
export 'window.dart';
|
||||
export 'windows.dart';
|
||||
export 'render.dart';
|
||||
@@ -73,7 +73,7 @@ const hotKeyActionListEquality = ListEquality<HotKeyAction>();
|
||||
const stringAndStringMapEquality = MapEquality<String, String>();
|
||||
const stringAndStringMapEntryIterableEquality =
|
||||
IterableEquality<MapEntry<String, String>>();
|
||||
const stringAndIntQMapEquality = MapEquality<String, int?>();
|
||||
const delayMapEquality = MapEquality<String, Map<String, int?>>();
|
||||
const stringSetEquality = SetEquality<String>();
|
||||
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
|
||||
|
||||
@@ -88,3 +88,7 @@ const defaultPrimaryColor = Colors.brown;
|
||||
double getWidgetHeight(num lines) {
|
||||
return max(lines * 84 + (lines - 1) * 16, 0);
|
||||
}
|
||||
|
||||
final mainIsolate = "FlClashMainIsolate";
|
||||
|
||||
final serviceIsolate = "FlClashServiceIsolate";
|
||||
|
||||
@@ -10,8 +10,8 @@ extension CompleterExt<T> on Completer<T> {
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
required String functionName,
|
||||
}) {
|
||||
final realTimeout = timeout ?? const Duration(minutes: 1);
|
||||
Timer(realTimeout + moreDuration, () {
|
||||
final realTimeout = timeout ?? const Duration(seconds: 1);
|
||||
Timer(realTimeout + commonDuration, () {
|
||||
if (onLast != null) {
|
||||
onLast();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseNavigator {
|
||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
||||
if (!globalState.appController.isMobileView) {
|
||||
return await Navigator.of(context).push<T>(
|
||||
CommonDesktopRoute(
|
||||
builder: (context) => child,
|
||||
),
|
||||
);
|
||||
}
|
||||
return await Navigator.of(context).push<T>(
|
||||
CommonRoute(
|
||||
builder: (context) => child,
|
||||
@@ -11,6 +19,46 @@ class BaseNavigator {
|
||||
}
|
||||
}
|
||||
|
||||
class CommonDesktopRoute<T> extends PageRoute<T> {
|
||||
final Widget Function(BuildContext context) builder;
|
||||
|
||||
CommonDesktopRoute({
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final Widget result = builder(context);
|
||||
return Semantics(
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
Duration get reverseTransitionDuration => Duration(milliseconds: 200);
|
||||
}
|
||||
|
||||
class CommonRoute<T> extends MaterialPageRoute<T> {
|
||||
CommonRoute({
|
||||
required super.builder,
|
||||
|
||||
56
lib/common/render.dart
Normal file
56
lib/common/render.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class Render {
|
||||
static Render? _instance;
|
||||
bool _isPaused = false;
|
||||
final _dispatcher = SchedulerBinding.instance.platformDispatcher;
|
||||
FrameCallback? _beginFrame;
|
||||
VoidCallback? _drawFrame;
|
||||
|
||||
Render._internal();
|
||||
|
||||
factory Render() {
|
||||
_instance ??= Render._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
active() {
|
||||
resume();
|
||||
pause();
|
||||
}
|
||||
|
||||
pause() {
|
||||
debouncer.call(
|
||||
"render_pause",
|
||||
_pause,
|
||||
duration: Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
|
||||
resume() {
|
||||
debouncer.cancel("render_pause");
|
||||
_resume();
|
||||
}
|
||||
|
||||
void _pause() {
|
||||
if (_isPaused) return;
|
||||
_isPaused = true;
|
||||
_beginFrame = _dispatcher.onBeginFrame;
|
||||
_drawFrame = _dispatcher.onDrawFrame;
|
||||
_dispatcher.onBeginFrame = null;
|
||||
_dispatcher.onDrawFrame = null;
|
||||
debugPrint("[App] pause");
|
||||
}
|
||||
|
||||
void _resume() {
|
||||
if (!_isPaused) return;
|
||||
_isPaused = false;
|
||||
_dispatcher.onBeginFrame = _beginFrame;
|
||||
_dispatcher.onDrawFrame = _drawFrame;
|
||||
debugPrint("[App] resume");
|
||||
}
|
||||
}
|
||||
|
||||
final render = system.isDesktop ? Render() : null;
|
||||
@@ -63,6 +63,7 @@ class Window {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
render?.resume();
|
||||
}
|
||||
|
||||
Future<bool> isVisible() async {
|
||||
@@ -76,6 +77,7 @@ class Window {
|
||||
hide() async {
|
||||
await windowManager.hide();
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
render?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,13 +76,10 @@ class AppController {
|
||||
|
||||
updateStatus(bool isStart) async {
|
||||
if (isStart) {
|
||||
await globalState.handleStart();
|
||||
updateRunTime();
|
||||
updateTraffic();
|
||||
globalState.updateFunctionLists = [
|
||||
await globalState.handleStart([
|
||||
updateRunTime,
|
||||
updateTraffic,
|
||||
];
|
||||
]);
|
||||
final currentLastModified =
|
||||
await config.getCurrentProfile()?.profileLastModified;
|
||||
if (currentLastModified == null ||
|
||||
@@ -163,6 +160,13 @@ class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
setProfile(Profile profile) {
|
||||
config.setProfile(profile);
|
||||
if (profile.id == config.currentProfile?.id) {
|
||||
applyProfileDebounce();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
@@ -279,8 +283,9 @@ class AppController {
|
||||
await clashService?.destroy();
|
||||
await proxy?.stopProxy();
|
||||
await savePreferences();
|
||||
} catch (_) {}
|
||||
system.exit();
|
||||
} finally {
|
||||
system.exit();
|
||||
}
|
||||
}
|
||||
|
||||
autoCheckUpdate() async {
|
||||
@@ -298,7 +303,7 @@ class AppController {
|
||||
final body = data['body'];
|
||||
final submits = other.parseReleaseBody(body);
|
||||
final textTheme = context.textTheme;
|
||||
globalState.showMessage(
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.discoverNewVersion,
|
||||
message: TextSpan(
|
||||
text: "$tagName \n",
|
||||
@@ -315,13 +320,14 @@ class AppController {
|
||||
),
|
||||
],
|
||||
),
|
||||
onTab: () {
|
||||
launchUrl(
|
||||
Uri.parse("https://github.com/$repository/releases/latest"),
|
||||
);
|
||||
},
|
||||
confirmText: appLocalizations.goDownload,
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
launchUrl(
|
||||
Uri.parse("https://github.com/$repository/releases/latest"),
|
||||
);
|
||||
} else if (handleError) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.checkUpdate,
|
||||
@@ -337,9 +343,6 @@ class AppController {
|
||||
if (!isDisclaimerAccepted) {
|
||||
handleExit();
|
||||
}
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
window?.show();
|
||||
}
|
||||
await globalState.initCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
@@ -351,11 +354,16 @@ class AppController {
|
||||
);
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
window?.show();
|
||||
} else {
|
||||
window?.hide();
|
||||
}
|
||||
}
|
||||
|
||||
_initStatus() async {
|
||||
if (Platform.isAndroid) {
|
||||
globalState.updateStartTime();
|
||||
await globalState.updateStartTime();
|
||||
}
|
||||
final status =
|
||||
globalState.isStart == true ? true : config.appSetting.autoRun;
|
||||
@@ -370,7 +378,10 @@ class AppController {
|
||||
appState.setDelay(delay);
|
||||
}
|
||||
|
||||
toPage(int index, {bool hasAnimate = false}) {
|
||||
toPage(
|
||||
int index, {
|
||||
bool hasAnimate = false,
|
||||
}) {
|
||||
if (index > appState.currentNavigationItems.length - 1) {
|
||||
return;
|
||||
}
|
||||
@@ -397,8 +408,8 @@ class AppController {
|
||||
|
||||
initLink() {
|
||||
linkManager.initAppLinksListen(
|
||||
(url) {
|
||||
globalState.showMessage(
|
||||
(url) async {
|
||||
final res = await globalState.showMessage(
|
||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||
message: TextSpan(
|
||||
children: [
|
||||
@@ -416,10 +427,12 @@ class AppController {
|
||||
"${appLocalizations.create}${appLocalizations.profile}"),
|
||||
],
|
||||
),
|
||||
onTab: () {
|
||||
addProfileFormURL(url);
|
||||
},
|
||||
);
|
||||
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
addProfileFormURL(url);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -522,6 +535,18 @@ class AppController {
|
||||
});
|
||||
}
|
||||
|
||||
int? getDelay(String proxyName, [String? url]) {
|
||||
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
|
||||
return currentDelayMap?[appState.getRealProxyName(proxyName)];
|
||||
}
|
||||
|
||||
String getRealTestUrl(String? url) {
|
||||
if (url == null || url.isEmpty) {
|
||||
return config.appSetting.testUrl;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
@@ -532,12 +557,12 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfDelay(List<Proxy> proxies) {
|
||||
return proxies = List.of(proxies)
|
||||
List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) {
|
||||
final aDelay = appState.getDelay(a.name);
|
||||
final bDelay = appState.getDelay(b.name);
|
||||
final aDelay = getDelay(a.name, url);
|
||||
final bDelay = getDelay(b.name, url);
|
||||
if (aDelay == null && bDelay == null) {
|
||||
return 0;
|
||||
}
|
||||
@@ -552,10 +577,10 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies) {
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
|
||||
return switch (config.proxiesStyle.sortType) {
|
||||
ProxiesSortType.none => proxies,
|
||||
ProxiesSortType.delay => _sortOfDelay(proxies),
|
||||
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
|
||||
ProxiesSortType.name => _sortOfName(proxies),
|
||||
};
|
||||
}
|
||||
@@ -580,6 +605,10 @@ class AppController {
|
||||
});
|
||||
}
|
||||
|
||||
bool get isMobileView {
|
||||
return appState.viewMode == ViewMode.mobile;
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
|
||||
@@ -100,15 +100,12 @@ enum AppMessageType {
|
||||
log,
|
||||
delay,
|
||||
request,
|
||||
started,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum ServiceMessageType {
|
||||
enum InvokeMessageType {
|
||||
protect,
|
||||
process,
|
||||
started,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum FindProcessMode { always, off }
|
||||
@@ -241,6 +238,17 @@ enum ActionMethod {
|
||||
stopListener,
|
||||
getCountryCode,
|
||||
getMemory,
|
||||
|
||||
///Android,
|
||||
setFdMap,
|
||||
setProcessMap,
|
||||
setState,
|
||||
startTun,
|
||||
stopTun,
|
||||
getRunTime,
|
||||
updateDns,
|
||||
getAndroidVpnOptions,
|
||||
getCurrentProfileName,
|
||||
}
|
||||
|
||||
enum AuthorizeCode { none, success, error }
|
||||
|
||||
@@ -40,7 +40,7 @@ class UsageSwitch extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.onlyProxy,
|
||||
selector: (_, config) => config.appSetting.onlyStatisticsProxy,
|
||||
builder: (_, onlyProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
@@ -50,7 +50,7 @@ class UsageSwitch extends StatelessWidget {
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
onlyProxy: value,
|
||||
onlyStatisticsProxy: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -225,7 +225,7 @@ class FakeIpFilterItem extends StatelessWidget {
|
||||
title: appLocalizations.fakeipFilter,
|
||||
items: fakeIpFilter,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -260,7 +260,7 @@ class DefaultNameserverItem extends StatelessWidget {
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -295,7 +295,7 @@ class NameserverItem extends StatelessWidget {
|
||||
title: "域名服务器",
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -384,7 +384,7 @@ class NameserverPolicyItem extends StatelessWidget {
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -419,7 +419,7 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
||||
title: appLocalizations.proxyNameserver,
|
||||
items: proxyServerNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -454,7 +454,7 @@ class FallbackItem extends StatelessWidget {
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -555,7 +555,7 @@ class GeositeItem extends StatelessWidget {
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -591,7 +591,7 @@ class IpcidrItem extends StatelessWidget {
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -627,7 +627,7 @@ class DomainItem extends StatelessWidget {
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items){
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
@@ -709,16 +709,17 @@ class DnsListView extends StatelessWidget {
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
globalState.appController.clashConfig.dns = defaultDns;
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.clashConfig.dns = defaultDns;
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -210,19 +210,19 @@ class BypassDomainItem extends StatelessWidget {
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
@@ -382,19 +382,19 @@ class NetworkListView extends StatelessWidget {
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.reset,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.resetTip,
|
||||
),
|
||||
onTab: () {
|
||||
final appController = globalState.appController;
|
||||
appController.config.vpnProps = defaultVpnProps;
|
||||
appController.clashConfig.tun = defaultTun;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.config.vpnProps = defaultVpnProps;
|
||||
appController.clashConfig.tun = defaultTun;
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -6,8 +7,9 @@ import 'package:fl_clash/models/common.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final _memoryInfoStateNotifier =
|
||||
ValueNotifier<TrafficValue>(TrafficValue(value: 0));
|
||||
final _memoryInfoStateNotifier = ValueNotifier<TrafficValue>(
|
||||
TrafficValue(value: 0),
|
||||
);
|
||||
|
||||
class MemoryInfo extends StatefulWidget {
|
||||
const MemoryInfo({super.key});
|
||||
@@ -22,10 +24,7 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
clashCore.getMemory().then((memory) {
|
||||
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
|
||||
});
|
||||
_updateMemoryData();
|
||||
_updateMemory();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -34,11 +33,15 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_updateMemoryData() {
|
||||
timer = Timer(Duration(seconds: 2), () async {
|
||||
final memory = await clashCore.getMemory();
|
||||
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
|
||||
_updateMemoryData();
|
||||
_updateMemory() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final rss = ProcessInfo.currentRss;
|
||||
_memoryInfoStateNotifier.value = TrafficValue(
|
||||
value: clashLib != null ? rss : await clashCore.getMemory() + rss,
|
||||
);
|
||||
timer = Timer(Duration(seconds: 5), () async {
|
||||
_updateMemory();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -76,41 +76,11 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
-16,
|
||||
-20,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.arrow_upward,
|
||||
color: color,
|
||||
size: 16,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
Text(
|
||||
"${_getLastTraffic(traffics).up}/s",
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_downward,
|
||||
color: color,
|
||||
size: 16,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
Text(
|
||||
"${_getLastTraffic(traffics).down}/s",
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
"${_getLastTraffic(traffics).up}↑ ${_getLastTraffic(traffics).down}↓",
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/pages/editor.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class EditProfile extends StatefulWidget {
|
||||
final Profile profile;
|
||||
@@ -30,10 +30,13 @@ class _EditProfileState extends State<EditProfile> {
|
||||
late TextEditingController urlController;
|
||||
late TextEditingController autoUpdateDurationController;
|
||||
late bool autoUpdate;
|
||||
String? rawText;
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
||||
Uint8List? fileData;
|
||||
|
||||
Profile get profile => widget.profile;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -51,28 +54,43 @@ class _EditProfileState extends State<EditProfile> {
|
||||
|
||||
_handleConfirm() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
final config = widget.context.read<Config>();
|
||||
final profile = widget.profile.copyWith(
|
||||
url: urlController.text,
|
||||
label: labelController.text,
|
||||
autoUpdate: autoUpdate,
|
||||
autoUpdateDuration: Duration(
|
||||
minutes: int.parse(
|
||||
autoUpdateDurationController.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
final appController = globalState.appController;
|
||||
Profile profile = this.profile.copyWith(
|
||||
url: urlController.text,
|
||||
label: labelController.text,
|
||||
autoUpdate: autoUpdate,
|
||||
autoUpdateDuration: Duration(
|
||||
minutes: int.parse(
|
||||
autoUpdateDurationController.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
final hasUpdate = widget.profile.url != profile.url;
|
||||
if (fileData != null) {
|
||||
config.setProfile(await profile.saveFile(fileData!));
|
||||
if (profile.type == ProfileType.url && autoUpdate) {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.profileHasUpdate,
|
||||
),
|
||||
);
|
||||
if (res == true) {
|
||||
profile = profile.copyWith(
|
||||
autoUpdate: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
appController.setProfile(await profile.saveFile(fileData!));
|
||||
} else if (!hasUpdate) {
|
||||
appController.setProfile(profile);
|
||||
} else {
|
||||
config.setProfile(profile);
|
||||
}
|
||||
if (hasUpdate) {
|
||||
globalState.homeScaffoldKey.currentState?.loadingRun(
|
||||
() async {
|
||||
await Future.delayed(
|
||||
commonDuration,
|
||||
);
|
||||
if (hasUpdate) {
|
||||
await globalState.appController.updateProfile(profile);
|
||||
await appController.updateProfile(profile);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -102,22 +120,69 @@ class _EditProfileState extends State<EditProfile> {
|
||||
);
|
||||
}
|
||||
|
||||
_editProfileFile() async {
|
||||
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
||||
if (profilePath == null) return;
|
||||
globalState.safeRun(() async {
|
||||
if (Platform.isAndroid) {
|
||||
await app?.openFile(
|
||||
profilePath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await launchUrl(
|
||||
Uri.file(
|
||||
profilePath,
|
||||
),
|
||||
_handleSaveEdit(BuildContext context, String data) async {
|
||||
final message = await globalState.safeRun<String>(
|
||||
() async {
|
||||
final message = await clashCore.validateConfig(data);
|
||||
return message;
|
||||
},
|
||||
silence: false,
|
||||
);
|
||||
if (message?.isNotEmpty == true) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: message),
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop(data);
|
||||
}
|
||||
}
|
||||
|
||||
_editProfileFile() async {
|
||||
if (rawText == null) {
|
||||
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
||||
if (profilePath == null) return;
|
||||
final file = File(profilePath);
|
||||
rawText = await file.readAsString();
|
||||
}
|
||||
if (!mounted) return;
|
||||
final title = widget.profile.label ?? widget.profile.id;
|
||||
final data = await BaseNavigator.push<String>(
|
||||
globalState.homeScaffoldKey.currentContext!,
|
||||
EditorPage(
|
||||
title: title,
|
||||
content: rawText!,
|
||||
onSave: _handleSaveEdit,
|
||||
onPop: (context, data) async {
|
||||
if (data == rawText) {
|
||||
return true;
|
||||
}
|
||||
final res = await globalState.showMessage(
|
||||
title: title,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.hasCacheChange,
|
||||
),
|
||||
);
|
||||
if (res == true && context.mounted) {
|
||||
_handleSaveEdit(context, data);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
rawText = data;
|
||||
fileData = Uint8List.fromList(utf8.encode(data));
|
||||
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
|
||||
size: fileData?.length ?? 0,
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
_uploadProfileFile() async {
|
||||
@@ -130,6 +195,20 @@ class _EditProfileState extends State<EditProfile> {
|
||||
);
|
||||
}
|
||||
|
||||
_handleBack() async {
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.fileIsUpdate),
|
||||
);
|
||||
if (res == true) {
|
||||
_handleConfirm();
|
||||
} else {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = [
|
||||
@@ -245,34 +324,45 @@ class _EditProfileState extends State<EditProfile> {
|
||||
},
|
||||
),
|
||||
];
|
||||
return FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
onPressed: _handleConfirm,
|
||||
label: Text(appLocalizations.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, __) {
|
||||
if (didPop) return;
|
||||
if (fileData == null) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
_handleBack();
|
||||
},
|
||||
child: FloatLayout(
|
||||
floatingWidget: FloatWrapper(
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
onPressed: _handleConfirm,
|
||||
label: Text(appLocalizations.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: kMaterialListPadding.copyWith(
|
||||
bottom: 72,
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: kMaterialListPadding.copyWith(
|
||||
bottom: 72,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
height: 24,
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
height: 24,
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,18 +7,11 @@ import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'add_profile.dart';
|
||||
|
||||
enum PopupMenuItemEnum { delete, edit }
|
||||
|
||||
enum ProfileActions {
|
||||
edit,
|
||||
update,
|
||||
delete,
|
||||
}
|
||||
|
||||
class ProfilesFragment extends StatefulWidget {
|
||||
const ProfilesFragment({super.key});
|
||||
|
||||
@@ -185,18 +178,16 @@ class ProfileItem extends StatelessWidget {
|
||||
});
|
||||
|
||||
_handleDeleteProfile(BuildContext context) async {
|
||||
globalState.showMessage(
|
||||
final res = await globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.deleteProfileTip,
|
||||
),
|
||||
onTab: () async {
|
||||
await globalState.appController.deleteProfile(profile.id);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
await globalState.appController.deleteProfile(profile.id);
|
||||
}
|
||||
|
||||
_handleUpdateProfile() async {
|
||||
@@ -266,6 +257,36 @@ class ProfileItem extends StatelessWidget {
|
||||
];
|
||||
}
|
||||
|
||||
_handleCopyLink(BuildContext context) async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: profile.url,
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.showNotifier(appLocalizations.copySuccess);
|
||||
}
|
||||
}
|
||||
|
||||
_handleExportFile(BuildContext context) async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final file = await profile.getFile();
|
||||
final value = await picker.saveFile(
|
||||
profile.label ?? profile.id,
|
||||
file.readAsBytesSync(),
|
||||
);
|
||||
if (value == null) return false;
|
||||
return true;
|
||||
},
|
||||
title: appLocalizations.tip,
|
||||
);
|
||||
if (res == true && context.mounted) {
|
||||
context.showNotifier(appLocalizations.exportSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
@@ -286,46 +307,59 @@ class ProfileItem extends StatelessWidget {
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonPopupMenu<ProfileActions>(
|
||||
icon: Icon(Icons.more_vert),
|
||||
items: [
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.edit,
|
||||
label: appLocalizations.edit,
|
||||
iconData: Icons.edit,
|
||||
),
|
||||
if (profile.type == ProfileType.url)
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.update,
|
||||
label: appLocalizations.update,
|
||||
iconData: Icons.sync,
|
||||
: CommonPopupBox(
|
||||
popup: CommonPopupMenu(
|
||||
items: [
|
||||
ActionItemData(
|
||||
icon: Icons.edit_outlined,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_handleShowEditExtendPage(context);
|
||||
},
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: ProfileActions.delete,
|
||||
label: appLocalizations.delete,
|
||||
iconData: Icons.delete,
|
||||
),
|
||||
],
|
||||
onSelected: (ProfileActions? action) async {
|
||||
switch (action) {
|
||||
case ProfileActions.edit:
|
||||
_handleShowEditExtendPage(context);
|
||||
break;
|
||||
case ProfileActions.delete:
|
||||
_handleDeleteProfile(context);
|
||||
break;
|
||||
case ProfileActions.update:
|
||||
_handleUpdateProfile();
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
if (profile.type == ProfileType.url) ...[
|
||||
ActionItemData(
|
||||
icon: Icons.sync_alt_sharp,
|
||||
label: appLocalizations.sync,
|
||||
onPressed: () {
|
||||
_handleUpdateProfile();
|
||||
},
|
||||
),
|
||||
ActionItemData(
|
||||
icon: Icons.copy,
|
||||
label: appLocalizations.copyLink,
|
||||
onPressed: () {
|
||||
_handleCopyLink(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
ActionItemData(
|
||||
icon: Icons.file_copy_outlined,
|
||||
label: appLocalizations.exportFile,
|
||||
onPressed: () {
|
||||
_handleExportFile(context);
|
||||
},
|
||||
),
|
||||
ActionItemData(
|
||||
icon: Icons.delete_outlined,
|
||||
iconSize: 20,
|
||||
label: appLocalizations.delete,
|
||||
onPressed: () {
|
||||
_handleDeleteProfile(context);
|
||||
},
|
||||
type: ActionType.danger,
|
||||
),
|
||||
],
|
||||
),
|
||||
target: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(Icons.more_vert),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:re_editor/re_editor.dart';
|
||||
import 'package:re_highlight/languages/yaml.dart';
|
||||
import 'package:re_highlight/styles/atom-one-light.dart';
|
||||
|
||||
class ViewProfile extends StatefulWidget {
|
||||
final Profile profile;
|
||||
|
||||
const ViewProfile({
|
||||
super.key,
|
||||
required this.profile,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ViewProfile> createState() => _ViewProfileState();
|
||||
}
|
||||
|
||||
class _ViewProfileState extends State<ViewProfile> {
|
||||
bool readOnly = true;
|
||||
final CodeLineEditingController _controller = CodeLineEditingController();
|
||||
final key = GlobalKey<CommonScaffoldState>();
|
||||
final _focusNode = FocusNode();
|
||||
String? rawText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
appPath.getProfilePath(widget.profile.id).then((path) async {
|
||||
if (path == null) return;
|
||||
final file = File(path);
|
||||
rawText = await file.readAsString();
|
||||
_controller.text = rawText ?? "";
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Profile get profile => widget.profile;
|
||||
|
||||
_handleChangeReadOnly() async {
|
||||
if (readOnly == true) {
|
||||
setState(() {
|
||||
readOnly = false;
|
||||
});
|
||||
} else {
|
||||
if (_controller.text == rawText) return;
|
||||
final newProfile = await key.currentState?.loadingRun<Profile>(() async {
|
||||
return await profile.saveFileWithString(_controller.text);
|
||||
});
|
||||
if (newProfile == null) return;
|
||||
globalState.appController.config.setProfile(newProfile);
|
||||
setState(() {
|
||||
readOnly = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonScaffold(
|
||||
key: key,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _controller.undo,
|
||||
icon: const Icon(Icons.undo),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _controller.redo,
|
||||
icon: const Icon(Icons.redo),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _handleChangeReadOnly,
|
||||
icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save),
|
||||
),
|
||||
],
|
||||
body: CodeEditor(
|
||||
readOnly: readOnly,
|
||||
focusNode: _focusNode,
|
||||
scrollbarBuilder: (context, child, details) {
|
||||
return Scrollbar(
|
||||
controller: details.controller,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(2),
|
||||
interactive: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
showCursorWhenReadOnly: false,
|
||||
controller: _controller,
|
||||
shortcutsActivatorsBuilder:
|
||||
const DefaultCodeShortcutsActivatorsBuilder(),
|
||||
indicatorBuilder: (
|
||||
context,
|
||||
editingController,
|
||||
chunkController,
|
||||
notifier,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
DefaultCodeLineNumber(
|
||||
controller: editingController,
|
||||
notifier: notifier,
|
||||
),
|
||||
DefaultCodeChunkIndicator(
|
||||
width: 20,
|
||||
controller: chunkController,
|
||||
notifier: notifier,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
toolbarController:
|
||||
!readOnly ? ContextMenuControllerImpl(_focusNode) : null,
|
||||
style: CodeEditorStyle(
|
||||
fontSize: 14,
|
||||
codeTheme: CodeHighlightTheme(
|
||||
languages: {
|
||||
'yaml': CodeHighlightThemeMode(
|
||||
mode: langYaml,
|
||||
)
|
||||
},
|
||||
theme: atomOneLightTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: widget.profile.label ?? widget.profile.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuItemWidget extends PopupMenuItem<void> {
|
||||
ContextMenuItemWidget({
|
||||
super.key,
|
||||
required String text,
|
||||
required VoidCallback super.onTap,
|
||||
}) : super(child: Text(text));
|
||||
}
|
||||
|
||||
class ContextMenuControllerImpl implements SelectionToolbarController {
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
ContextMenuControllerImpl(
|
||||
this.focusNode,
|
||||
);
|
||||
|
||||
_removeOverLayEntry() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void hide(BuildContext context) {
|
||||
// _removeOverLayEntry();
|
||||
}
|
||||
|
||||
// _handleCut(CodeLineEditingController controller) {
|
||||
// controller.cut();
|
||||
// _removeOverLayEntry();
|
||||
// }
|
||||
//
|
||||
// _handleCopy(CodeLineEditingController controller) async {
|
||||
// await controller.copy();
|
||||
// _removeOverLayEntry();
|
||||
// }
|
||||
//
|
||||
// _handlePaste(CodeLineEditingController controller) {
|
||||
// controller.paste();
|
||||
// _removeOverLayEntry();
|
||||
// }
|
||||
|
||||
@override
|
||||
void show({
|
||||
required BuildContext context,
|
||||
required CodeLineEditingController controller,
|
||||
required TextSelectionToolbarAnchors anchors,
|
||||
Rect? renderRect,
|
||||
required LayerLink layerLink,
|
||||
required ValueNotifier<bool> visibility,
|
||||
}) {
|
||||
if (controller.selectedText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_removeOverLayEntry();
|
||||
final relativeRect = RelativeRect.fromSize(
|
||||
(anchors.primaryAnchor) &
|
||||
const Size(150, double.infinity),
|
||||
MediaQuery.of(context).size,
|
||||
);
|
||||
_overlayEntry ??= OverlayEntry(
|
||||
builder: (context) => ValueListenableBuilder<CodeLineEditingValue>(
|
||||
valueListenable: controller,
|
||||
builder: (_, __, child) {
|
||||
if (controller.selectedText.isEmpty) {
|
||||
_removeOverLayEntry();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: Positioned(
|
||||
left: relativeRect.left,
|
||||
top: relativeRect.top,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
FocusScope.of(context).requestFocus(focusNode);
|
||||
},
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@ class ProxyCard extends StatelessWidget {
|
||||
final Proxy proxy;
|
||||
final GroupType groupType;
|
||||
final ProxyCardType type;
|
||||
final String? testUrl;
|
||||
|
||||
const ProxyCard({
|
||||
super.key,
|
||||
required this.groupName,
|
||||
required this.testUrl,
|
||||
required this.proxy,
|
||||
required this.groupType,
|
||||
required this.type,
|
||||
@@ -24,16 +26,18 @@ class ProxyCard extends StatelessWidget {
|
||||
Measure get measure => globalState.measure;
|
||||
|
||||
_handleTestCurrentDelay() {
|
||||
proxyDelayTest(proxy);
|
||||
proxyDelayTest(
|
||||
proxy,
|
||||
testUrl,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDelayText() {
|
||||
return SizedBox(
|
||||
height: measure.labelSmallHeight,
|
||||
child: Selector<AppState, int?>(
|
||||
selector: (context, appState) => appState.getDelay(
|
||||
proxy.name,
|
||||
),
|
||||
selector: (context, appState) =>
|
||||
globalState.appController.getDelay(proxy.name,testUrl),
|
||||
builder: (context, delay, __) {
|
||||
return FadeBox(
|
||||
child: Builder(
|
||||
|
||||
@@ -38,33 +38,48 @@ double getItemHeight(ProxyCardType proxyCardType) {
|
||||
};
|
||||
}
|
||||
|
||||
proxyDelayTest(Proxy proxy) async {
|
||||
proxyDelayTest(Proxy proxy, [String? testUrl]) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
||||
final url = appController.getRealTestUrl(testUrl);
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
url: url,
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
globalState.appController.setDelay(
|
||||
await clashCore.getDelay(
|
||||
url,
|
||||
proxyName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
delayTest(List<Proxy> proxies) async {
|
||||
delayTest(List<Proxy> proxies, [String? testUrl]) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyNames = proxies
|
||||
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
final url = appController.getRealTestUrl(testUrl);
|
||||
|
||||
final delayProxies = proxyNames.map<Future>((proxyName) async {
|
||||
globalState.appController.setDelay(
|
||||
Delay(
|
||||
url: url,
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||
globalState.appController.setDelay(
|
||||
await clashCore.getDelay(
|
||||
url,
|
||||
proxyName,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final batchesDelayProxies = delayProxies.batch(100);
|
||||
@@ -86,7 +101,7 @@ double getScrollToSelectedOffset({
|
||||
final proxyCardType = appController.config.proxiesStyle.cardType;
|
||||
final selectedName = appController.getCurrentSelectedName(groupName);
|
||||
final findSelectedIndex = proxies.indexWhere(
|
||||
(proxy) => proxy.name == selectedName,
|
||||
(proxy) => proxy.name == selectedName,
|
||||
);
|
||||
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
||||
final rows = (selectedIndex / columns).floor();
|
||||
|
||||
@@ -134,6 +134,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
if (isExpand) {
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
group.all,
|
||||
group.testUrl,
|
||||
);
|
||||
groupNameProxiesMap[groupName] = sortedProxies;
|
||||
final chunks = sortedProxies.chunks(columns);
|
||||
@@ -142,6 +143,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
.map<Widget>(
|
||||
(proxy) => Flexible(
|
||||
child: ProxyCard(
|
||||
testUrl: group.testUrl,
|
||||
type: type,
|
||||
groupType: group.type,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
@@ -259,6 +261,11 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
return prev != next;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
if (state.groupNames.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProxies,
|
||||
);
|
||||
}
|
||||
final items = _buildItems(
|
||||
groupNames: state.groupNames,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
@@ -367,10 +374,13 @@ class _ListHeaderState extends State<ListHeader>
|
||||
|
||||
bool get isExpand => widget.isExpand;
|
||||
|
||||
_delayTest(List<Proxy> proxies) async {
|
||||
_delayTest() async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
await delayTest(proxies);
|
||||
await delayTest(
|
||||
widget.group.all,
|
||||
widget.group.testUrl,
|
||||
);
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
@@ -563,9 +573,7 @@ class _ListHeaderState extends State<ListHeader>
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_delayTest(widget.group.all);
|
||||
},
|
||||
onPressed: _delayTest,
|
||||
icon: const Icon(
|
||||
Icons.network_ping,
|
||||
),
|
||||
|
||||
@@ -47,21 +47,40 @@ class _ProvidersState extends State<Providers> {
|
||||
_updateProviders() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final providers = globalState.appController.appState.providers;
|
||||
final messages = [];
|
||||
final updateProviders = providers.map<Future>(
|
||||
(provider) async {
|
||||
appState.setProvider(
|
||||
provider.copyWith(isUpdating: true),
|
||||
);
|
||||
await clashCore.updateExternalProvider(
|
||||
final message = await clashCore.updateExternalProvider(
|
||||
providerName: provider.name,
|
||||
);
|
||||
if (message.isNotEmpty) {
|
||||
messages.add("${provider.name}: $message \n");
|
||||
}
|
||||
appState.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
},
|
||||
);
|
||||
final titleMedium = context.textTheme.titleMedium;
|
||||
await Future.wait(updateProviders);
|
||||
await globalState.appController.updateGroupsDebounce();
|
||||
if (messages.isNotEmpty) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
children: [
|
||||
for (final message in messages)
|
||||
TextSpan(
|
||||
text: message,
|
||||
style: titleMedium,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -107,10 +126,10 @@ class ProviderItem extends StatelessWidget {
|
||||
});
|
||||
|
||||
_handleUpdateProvider() async {
|
||||
await globalState.safeRun<void>(() async {
|
||||
final appState = globalState.appController.appState;
|
||||
if (provider.vehicleType != "HTTP") return;
|
||||
await globalState.safeRun(() async {
|
||||
final appState = globalState.appController.appState;
|
||||
if (provider.vehicleType != "HTTP") return;
|
||||
await globalState.safeRun(
|
||||
() async {
|
||||
appState.setProvider(
|
||||
provider.copyWith(
|
||||
isUpdating: true,
|
||||
@@ -120,11 +139,12 @@ class ProviderItem extends StatelessWidget {
|
||||
providerName: provider.name,
|
||||
);
|
||||
if (message.isNotEmpty) throw message;
|
||||
});
|
||||
appState.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
});
|
||||
},
|
||||
silence: false,
|
||||
);
|
||||
appState.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
await globalState.appController.updateGroupsDebounce();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'card.dart';
|
||||
import 'common.dart';
|
||||
|
||||
List<Proxy> currentProxies = [];
|
||||
String? currentTestUrl;
|
||||
|
||||
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
|
||||
|
||||
@@ -114,6 +115,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
}
|
||||
final currentGroup = currentGroups[index ?? _tabController!.index];
|
||||
currentProxies = currentGroup.all;
|
||||
currentTestUrl = currentGroup.testUrl;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appController.config.updateCurrentGroupName(
|
||||
currentGroup.name,
|
||||
@@ -161,6 +163,11 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
return false;
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
if (state.groupNames.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProxies,
|
||||
);
|
||||
}
|
||||
final index = state.groupNames.indexWhere(
|
||||
(item) => item == state.currentGroupName,
|
||||
);
|
||||
@@ -273,7 +280,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
_delayTest() async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
await delayTest(currentProxies);
|
||||
await delayTest(
|
||||
currentProxies,
|
||||
currentTestUrl,
|
||||
);
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
@@ -289,6 +299,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
}
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
currentProxies,
|
||||
currentTestUrl,
|
||||
);
|
||||
_controller.animateTo(
|
||||
min(
|
||||
@@ -334,6 +345,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
sortNum: appState.sortNum,
|
||||
proxies: group.all,
|
||||
groupType: group.type,
|
||||
testUrl: group.testUrl,
|
||||
);
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
@@ -342,6 +354,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
final proxyCardType = state.proxyCardType;
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
proxies,
|
||||
state.testUrl,
|
||||
);
|
||||
return ActiveBuilder(
|
||||
label: "proxies",
|
||||
@@ -369,6 +382,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
testUrl: state.testUrl,
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
|
||||
@@ -191,8 +191,10 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
final message = await clashCore.updateGeoData(
|
||||
geoName: geoItem.fileName,
|
||||
geoType: geoItem.label,
|
||||
UpdateGeoDataParams(
|
||||
geoName: geoItem.fileName,
|
||||
geoType: geoItem.label,
|
||||
),
|
||||
);
|
||||
if (message.isNotEmpty) throw message;
|
||||
} catch (e) {
|
||||
@@ -249,12 +251,8 @@ class UpdateGeoUrlFormDialog extends StatefulWidget {
|
||||
final String url;
|
||||
final String? defaultValue;
|
||||
|
||||
const UpdateGeoUrlFormDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.url,
|
||||
this.defaultValue
|
||||
});
|
||||
const UpdateGeoUrlFormDialog(
|
||||
{super.key, required this.title, required this.url, this.defaultValue});
|
||||
|
||||
@override
|
||||
State<UpdateGeoUrlFormDialog> createState() => _UpdateGeoUrlFormDialogState();
|
||||
|
||||
@@ -333,5 +333,13 @@
|
||||
"routeAddressDesc": "Config listen route address",
|
||||
"pleaseInputAdminPassword": "Please enter the admin password",
|
||||
"copyEnvVar": "Copying environment variables",
|
||||
"memoryInfo": "Memory info"
|
||||
"memoryInfo": "Memory info",
|
||||
"cancel": "Cancel",
|
||||
"fileIsUpdate": "The file has been modified. Do you want to save the changes?",
|
||||
"profileHasUpdate": "The profile has been modified. Do you want to disable auto update?",
|
||||
"hasCacheChange": "Do you want to cache the changes?",
|
||||
"nullProxies": "No proxies",
|
||||
"copySuccess": "Copy success",
|
||||
"copyLink": "Copy link",
|
||||
"exportFile": "Export file"
|
||||
}
|
||||
@@ -333,5 +333,13 @@
|
||||
"routeAddressDesc": "配置监听路由地址",
|
||||
"pleaseInputAdminPassword": "请输入管理员密码",
|
||||
"copyEnvVar": "复制环境变量",
|
||||
"memoryInfo": "内存信息"
|
||||
"memoryInfo": "内存信息",
|
||||
"cancel": "取消",
|
||||
"fileIsUpdate": "文件有修改,是否保存修改",
|
||||
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
|
||||
"hasCacheChange": "是否缓存修改",
|
||||
"nullProxies": "暂无代理",
|
||||
"copySuccess": "复制成功",
|
||||
"copyLink": "复制链接",
|
||||
"exportFile": "导出文件"
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"bypassDomain": MessageLookupByLibrary.simpleMessage("Bypass domain"),
|
||||
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Only takes effect when the system proxy is enabled"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||
"cancelFilterSystemApp":
|
||||
MessageLookupByLibrary.simpleMessage("Cancel filter system app"),
|
||||
"cancelSelectAll":
|
||||
@@ -123,6 +124,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
|
||||
"copyEnvVar": MessageLookupByLibrary.simpleMessage(
|
||||
"Copying environment variables"),
|
||||
"copyLink": MessageLookupByLibrary.simpleMessage("Copy link"),
|
||||
"copySuccess": MessageLookupByLibrary.simpleMessage("Copy success"),
|
||||
"core": MessageLookupByLibrary.simpleMessage("Core"),
|
||||
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("Country"),
|
||||
@@ -170,6 +173,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||
"expirationTime":
|
||||
MessageLookupByLibrary.simpleMessage("Expiration time"),
|
||||
"exportFile": MessageLookupByLibrary.simpleMessage("Export file"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("Export logs"),
|
||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("Export Success"),
|
||||
"externalController":
|
||||
@@ -189,6 +193,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"file": MessageLookupByLibrary.simpleMessage("File"),
|
||||
"fileDesc":
|
||||
MessageLookupByLibrary.simpleMessage("Directly upload profile"),
|
||||
"fileIsUpdate": MessageLookupByLibrary.simpleMessage(
|
||||
"The file has been modified. Do you want to save the changes?"),
|
||||
"filterSystemApp":
|
||||
MessageLookupByLibrary.simpleMessage("Filter system app"),
|
||||
"findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"),
|
||||
@@ -208,6 +214,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"global": MessageLookupByLibrary.simpleMessage("Global"),
|
||||
"go": MessageLookupByLibrary.simpleMessage("Go"),
|
||||
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
|
||||
"hasCacheChange": MessageLookupByLibrary.simpleMessage(
|
||||
"Do you want to cache the changes?"),
|
||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("Add Hosts"),
|
||||
"hotkeyConflict":
|
||||
MessageLookupByLibrary.simpleMessage("Hotkey conflict"),
|
||||
@@ -303,6 +311,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("No logs"),
|
||||
"nullProfileDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"No profile, Please add a profile"),
|
||||
"nullProxies": MessageLookupByLibrary.simpleMessage("No proxies"),
|
||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
|
||||
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
|
||||
"onlyIcon": MessageLookupByLibrary.simpleMessage("Icon"),
|
||||
@@ -348,6 +357,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"profileAutoUpdateIntervalNullValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Please enter the auto update interval time"),
|
||||
"profileHasUpdate": MessageLookupByLibrary.simpleMessage(
|
||||
"The profile has been modified. Do you want to disable auto update?"),
|
||||
"profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Please input the profile name"),
|
||||
"profileParseErrorDesc":
|
||||
|
||||
@@ -80,6 +80,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
|
||||
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
|
||||
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||
"cancelFilterSystemApp":
|
||||
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
|
||||
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
|
||||
@@ -99,6 +100,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
|
||||
"copy": MessageLookupByLibrary.simpleMessage("复制"),
|
||||
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
|
||||
"copyLink": MessageLookupByLibrary.simpleMessage("复制链接"),
|
||||
"copySuccess": MessageLookupByLibrary.simpleMessage("复制成功"),
|
||||
"core": MessageLookupByLibrary.simpleMessage("内核"),
|
||||
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("区域"),
|
||||
@@ -138,6 +141,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
||||
"expand": MessageLookupByLibrary.simpleMessage("标准"),
|
||||
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
||||
"exportFile": MessageLookupByLibrary.simpleMessage("导出文件"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
|
||||
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
||||
@@ -152,6 +156,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
|
||||
"file": MessageLookupByLibrary.simpleMessage("文件"),
|
||||
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
|
||||
"fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"),
|
||||
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
|
||||
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
|
||||
"findProcessModeDesc":
|
||||
@@ -168,6 +173,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"global": MessageLookupByLibrary.simpleMessage("全局"),
|
||||
"go": MessageLookupByLibrary.simpleMessage("前往"),
|
||||
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
|
||||
"hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"),
|
||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
|
||||
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
|
||||
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
|
||||
@@ -241,6 +247,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
|
||||
"nullProfileDesc":
|
||||
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
|
||||
"nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
|
||||
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
|
||||
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"),
|
||||
@@ -275,6 +282,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
|
||||
"profileAutoUpdateIntervalNullValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"),
|
||||
"profileHasUpdate":
|
||||
MessageLookupByLibrary.simpleMessage("配置文件已经修改,是否关闭自动更新 "),
|
||||
"profileNameNullValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请输入配置名称"),
|
||||
"profileParseErrorDesc":
|
||||
|
||||
@@ -3399,6 +3399,86 @@ class AppLocalizations {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Cancel`
|
||||
String get cancel {
|
||||
return Intl.message(
|
||||
'Cancel',
|
||||
name: 'cancel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The file has been modified. Do you want to save the changes?`
|
||||
String get fileIsUpdate {
|
||||
return Intl.message(
|
||||
'The file has been modified. Do you want to save the changes?',
|
||||
name: 'fileIsUpdate',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The profile has been modified. Do you want to disable auto update?`
|
||||
String get profileHasUpdate {
|
||||
return Intl.message(
|
||||
'The profile has been modified. Do you want to disable auto update?',
|
||||
name: 'profileHasUpdate',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Do you want to cache the changes?`
|
||||
String get hasCacheChange {
|
||||
return Intl.message(
|
||||
'Do you want to cache the changes?',
|
||||
name: 'hasCacheChange',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No proxies`
|
||||
String get nullProxies {
|
||||
return Intl.message(
|
||||
'No proxies',
|
||||
name: 'nullProxies',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Copy success`
|
||||
String get copySuccess {
|
||||
return Intl.message(
|
||||
'Copy success',
|
||||
name: 'copySuccess',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Copy link`
|
||||
String get copyLink {
|
||||
return Intl.message(
|
||||
'Copy link',
|
||||
name: 'copyLink',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Export file`
|
||||
String get exportFile {
|
||||
return Intl.message(
|
||||
'Export file',
|
||||
name: 'exportFile',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
240
lib/main.dart
240
lib/main.dart
@@ -1,7 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/plugins/tile.dart';
|
||||
import 'package:fl_clash/plugins/vpn.dart';
|
||||
@@ -10,13 +14,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import 'application.dart';
|
||||
import 'clash/core.dart';
|
||||
import 'clash/lib.dart';
|
||||
import 'common/common.dart';
|
||||
import 'l10n/l10n.dart';
|
||||
import 'models/models.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
globalState.isService = false;
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
clashLib?.initMessage();
|
||||
await clashCore.preload();
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final version = await system.version;
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
@@ -54,126 +61,121 @@ Future<void> main() async {
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> vpnService() async {
|
||||
Future<void> _service(List<String> flags) async {
|
||||
globalState.isService = true;
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
globalState.isVpnService = true;
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final version = await system.version;
|
||||
final quickStart = flags.contains("quick");
|
||||
final clashLibHandler = ClashLibHandler();
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
|
||||
await AppLocalizations.load(
|
||||
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);
|
||||
clashLib?.setFdMap(fd.id);
|
||||
},
|
||||
onProcess: (ProcessData process) async {
|
||||
final packageName = await vpn?.resolverProcess(process);
|
||||
clashLib?.setProcessMap(
|
||||
ProcessMapItem(
|
||||
id: process.id,
|
||||
value: packageName ?? "",
|
||||
),
|
||||
);
|
||||
},
|
||||
onLoaded: (String groupName) {
|
||||
final currentSelectedMap = config.currentSelectedMap;
|
||||
final proxyName = currentSelectedMap[groupName];
|
||||
if (proxyName == null) return;
|
||||
globalState.changeProxy(
|
||||
config: config,
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
);
|
||||
tile?.addListener(
|
||||
_TileListenerWithService(
|
||||
onStop: () async {
|
||||
await app?.tip(appLocalizations.stopVpn);
|
||||
clashLibHandler.stopListener();
|
||||
clashLibHandler.stopTun();
|
||||
await vpn?.stop();
|
||||
exit(0);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (!quickStart) {
|
||||
_handleMainIpc(clashLibHandler);
|
||||
} else {
|
||||
await ClashCore.initGeo();
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
await app?.tip(appLocalizations.startVpn);
|
||||
clashLibHandler
|
||||
.quickStart(
|
||||
homeDirPath,
|
||||
globalState.getUpdateConfigParams(config, clashConfig, false),
|
||||
globalState.getCoreState(config, clashConfig),
|
||||
)
|
||||
.then(
|
||||
(res) async {
|
||||
await vpn?.start(
|
||||
clashLibHandler.getAndroidVpnOptions(),
|
||||
);
|
||||
clashLibHandler.startListener();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
vpn?.handleGetStartForegroundParams = () {
|
||||
final traffic = clashLibHandler.getTraffic();
|
||||
return json.encode({
|
||||
"title": clashLibHandler.getCurrentProfileName(),
|
||||
"content": "$traffic"
|
||||
});
|
||||
};
|
||||
|
||||
vpn?.addListener(
|
||||
_VpnListenerWithService(
|
||||
onStarted: (int fd) {
|
||||
clashLibHandler.startTun(fd);
|
||||
},
|
||||
onDnsChanged: (String dns) {
|
||||
clashLibHandler.updateDns(dns);
|
||||
},
|
||||
),
|
||||
);
|
||||
final invokeReceiverPort = ReceivePort();
|
||||
clashLibHandler.attachInvokePort(
|
||||
invokeReceiverPort.sendPort.nativePort,
|
||||
);
|
||||
invokeReceiverPort.listen(
|
||||
(message) async {
|
||||
final invokeMessage = InvokeMessage.fromJson(json.decode(message));
|
||||
switch (invokeMessage.type) {
|
||||
case InvokeMessageType.protect:
|
||||
final fd = Fd.fromJson(invokeMessage.data);
|
||||
await vpn?.setProtect(fd.value);
|
||||
clashLibHandler.setFdMap(fd.id);
|
||||
case InvokeMessageType.process:
|
||||
final process = ProcessData.fromJson(invokeMessage.data);
|
||||
final processName = await vpn?.resolverProcess(process) ?? "";
|
||||
clashLibHandler.setProcessMap(
|
||||
ProcessMapItem(
|
||||
id: process.id,
|
||||
value: processName,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_handleMainIpc(ClashLibHandler clashLibHandler) {
|
||||
final sendPort = IsolateNameServer.lookupPortByName(mainIsolate);
|
||||
if (sendPort == null) {
|
||||
return;
|
||||
}
|
||||
final serviceReceiverPort = ReceivePort();
|
||||
serviceReceiverPort.listen((message) async {
|
||||
final res = await clashLibHandler.invokeAction(message);
|
||||
sendPort.send(res);
|
||||
});
|
||||
sendPort.send(serviceReceiverPort.sendPort);
|
||||
final messageReceiverPort = ReceivePort();
|
||||
clashLibHandler.attachMessagePort(
|
||||
messageReceiverPort.sendPort.nativePort,
|
||||
);
|
||||
messageReceiverPort.listen((message) {
|
||||
sendPort.send(message);
|
||||
});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ServiceMessageHandler with ServiceMessageListener {
|
||||
final Function(Fd fd) _onProtect;
|
||||
final Function(ProcessData process) _onProcess;
|
||||
final Function(String providerName) _onLoaded;
|
||||
|
||||
const ServiceMessageHandler({
|
||||
required Function(Fd fd) onProtect,
|
||||
required Function(ProcessData process) onProcess,
|
||||
required Function(String providerName) onLoaded,
|
||||
}) : _onProtect = onProtect,
|
||||
_onProcess = onProcess,
|
||||
_onLoaded = onLoaded;
|
||||
|
||||
@override
|
||||
onProtect(Fd fd) {
|
||||
_onProtect(fd);
|
||||
}
|
||||
|
||||
@override
|
||||
onProcess(ProcessData process) {
|
||||
_onProcess(process);
|
||||
}
|
||||
|
||||
@override
|
||||
onLoaded(String providerName) {
|
||||
_onLoaded(providerName);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class TileListenerWithVpn with TileListener {
|
||||
class _TileListenerWithService with TileListener {
|
||||
final Function() _onStop;
|
||||
|
||||
const TileListenerWithVpn({
|
||||
const _TileListenerWithService({
|
||||
required Function() onStop,
|
||||
}) : _onStop = onStop;
|
||||
|
||||
@@ -182,3 +184,27 @@ class TileListenerWithVpn with TileListener {
|
||||
_onStop();
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class _VpnListenerWithService with VpnListener {
|
||||
final Function(int fd) _onStarted;
|
||||
final Function(String dns) _onDnsChanged;
|
||||
|
||||
const _VpnListenerWithService({
|
||||
required Function(int fd) onStarted,
|
||||
required Function(String dns) onDnsChanged,
|
||||
}) : _onStarted = onStarted,
|
||||
_onDnsChanged = onDnsChanged;
|
||||
|
||||
@override
|
||||
void onStarted(int fd) {
|
||||
super.onStarted(fd);
|
||||
_onStarted(fd);
|
||||
}
|
||||
|
||||
@override
|
||||
void onDnsChanged(String dns) {
|
||||
super.onDnsChanged(dns);
|
||||
_onDnsChanged(dns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -26,17 +27,9 @@ class _AndroidContainerState extends State<AndroidManager> {
|
||||
|
||||
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,
|
||||
selector: (_, config, clashConfig) => globalState.getCoreState(
|
||||
config,
|
||||
clashConfig,
|
||||
),
|
||||
builder: (__, state, child) {
|
||||
clashLib?.setState(state);
|
||||
|
||||
@@ -21,11 +21,10 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
_updateNavigationsContainer(Widget child) {
|
||||
return Selector2<AppState, Config, UpdateNavigationsSelector>(
|
||||
selector: (_, appState, config) {
|
||||
final group = appState.currentGroups;
|
||||
final hasProfile = config.profiles.isNotEmpty;
|
||||
return UpdateNavigationsSelector(
|
||||
openLogs: config.appSetting.openLogs,
|
||||
hasProxies: group.isNotEmpty && hasProfile,
|
||||
hasProxies: hasProfile && config.currentProfileId != null,
|
||||
);
|
||||
},
|
||||
builder: (context, state, child) {
|
||||
@@ -74,9 +73,12 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
|
||||
@override
|
||||
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
final isPaused = state == AppLifecycleState.paused;
|
||||
if (isPaused) {
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive) {
|
||||
globalState.appController.savePreferencesDebounce();
|
||||
render?.pause();
|
||||
} else {
|
||||
render?.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +90,14 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _cacheStateChange(
|
||||
_updateNavigationsContainer(
|
||||
widget.child,
|
||||
return Listener(
|
||||
onPointerDown: (_) {
|
||||
render?.resume();
|
||||
},
|
||||
child: _cacheStateChange(
|
||||
_updateNavigationsContainer(
|
||||
widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,14 +99,13 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
|
||||
|
||||
@override
|
||||
Future<void> onDelay(Delay delay) async {
|
||||
super.onDelay(delay);
|
||||
final appController = globalState.appController;
|
||||
appController.setDelay(delay);
|
||||
super.onDelay(delay);
|
||||
debouncer.call(
|
||||
DebounceTag.updateDelay,
|
||||
() async {
|
||||
await appController.updateGroupsDebounce();
|
||||
// await appController.addCheckIpNumDebounce();
|
||||
},
|
||||
duration: const Duration(milliseconds: 5000),
|
||||
);
|
||||
@@ -121,12 +120,6 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
|
||||
super.onLog(log);
|
||||
}
|
||||
|
||||
@override
|
||||
void onStarted(String runTime) {
|
||||
super.onStarted(runTime);
|
||||
globalState.appController.applyProfileDebounce();
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(Connection connection) async {
|
||||
globalState.appController.appState.addRequest(connection);
|
||||
|
||||
@@ -19,45 +19,26 @@ class MessageManager extends StatefulWidget {
|
||||
|
||||
class MessageManagerState extends State<MessageManager>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _floatMessageKey = GlobalKey();
|
||||
List<CommonMessage> bufferMessages = [];
|
||||
final _messagesNotifier = ValueNotifier<List<CommonMessage>>([]);
|
||||
final _floatMessageNotifier = ValueNotifier<CommonMessage?>(null);
|
||||
double maxWidth = 0;
|
||||
Offset offset = Offset.zero;
|
||||
|
||||
late AnimationController _animationController;
|
||||
|
||||
Completer? _animationCompleter;
|
||||
late Animation<Offset> _floatOffsetAnimation;
|
||||
late Animation<Offset> _commonOffsetAnimation;
|
||||
final animationDuration = commonDuration * 2;
|
||||
|
||||
_initTransformState() {
|
||||
_floatMessageNotifier.value = null;
|
||||
_floatOffsetAnimation = Tween(
|
||||
begin: Offset.zero,
|
||||
end: Offset.zero,
|
||||
).animate(_animationController);
|
||||
_commonOffsetAnimation = _floatOffsetAnimation = Tween(
|
||||
begin: Offset.zero,
|
||||
end: Offset.zero,
|
||||
).animate(_animationController);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 200),
|
||||
duration: Duration(milliseconds: 400),
|
||||
);
|
||||
_initTransformState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messagesNotifier.dispose();
|
||||
_floatMessageNotifier.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -67,126 +48,13 @@ class MessageManagerState extends State<MessageManager>
|
||||
id: other.uuidV4,
|
||||
text: text,
|
||||
);
|
||||
bufferMessages.add(commonMessage);
|
||||
await _animationCompleter?.future;
|
||||
_showMessage();
|
||||
}
|
||||
|
||||
_showMessage() {
|
||||
final commonMessage = bufferMessages.removeAt(0);
|
||||
_floatOffsetAnimation = Tween(
|
||||
begin: Offset(-maxWidth, 0),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Interval(
|
||||
0.5,
|
||||
1,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
_floatMessageNotifier.value = commonMessage;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final size = _floatMessageKey.currentContext?.size ?? Size.zero;
|
||||
_commonOffsetAnimation = Tween(
|
||||
begin: Offset.zero,
|
||||
end: Offset(0, -size.height - 12),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Interval(
|
||||
0,
|
||||
0.7,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
),
|
||||
_messagesNotifier.value = List.from(_messagesNotifier.value)
|
||||
..add(
|
||||
commonMessage,
|
||||
);
|
||||
_animationCompleter = Completer();
|
||||
_animationCompleter?.complete(_animationController.forward(from: 0));
|
||||
await _animationCompleter?.future;
|
||||
_initTransformState();
|
||||
_messagesNotifier.value = List.from(_messagesNotifier.value)
|
||||
..add(commonMessage);
|
||||
Future.delayed(
|
||||
commonMessage.duration,
|
||||
() {
|
||||
_removeMessage(commonMessage);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _wrapOffset(Widget child) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController.view,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: _commonOffsetAnimation.value,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapMessage(CommonMessage message) {
|
||||
return Material(
|
||||
elevation: 2,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: context.colorScheme.secondaryFixedDim,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 15),
|
||||
child: Text(
|
||||
message.text,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSecondaryFixedVariant,
|
||||
),
|
||||
maxLines: 5,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _floatMessage() {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _floatMessageNotifier,
|
||||
builder: (_, message, ___) {
|
||||
if (message == null) {
|
||||
return SizedBox();
|
||||
}
|
||||
return AnimatedBuilder(
|
||||
key: _floatMessageKey,
|
||||
animation: _animationController.view,
|
||||
builder: (_, child) {
|
||||
if (!_animationController.isAnimating) {
|
||||
return Opacity(
|
||||
opacity: 0,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return Transform.translate(
|
||||
offset: _floatOffsetAnimation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _wrapMessage(
|
||||
message,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_removeMessage(CommonMessage commonMessage) async {
|
||||
final itemWrapState = GlobalObjectKey(commonMessage.id).currentState
|
||||
as _MessageItemWrapState?;
|
||||
await itemWrapState?.transform(
|
||||
Offset(-maxWidth, 0),
|
||||
);
|
||||
_handleRemove(CommonMessage commonMessage) async {
|
||||
_messagesNotifier.value = List<CommonMessage>.from(_messagesNotifier.value)
|
||||
..remove(commonMessage);
|
||||
}
|
||||
@@ -204,6 +72,7 @@ class MessageManagerState extends State<MessageManager>
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: globalState.safeMessageOffsetNotifier,
|
||||
builder: (_, offset, child) {
|
||||
this.offset = offset;
|
||||
if (offset == Offset.zero) {
|
||||
return SizedBox();
|
||||
}
|
||||
@@ -234,15 +103,14 @@ class MessageManagerState extends State<MessageManager>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final message in messages) ...[
|
||||
if (message != messages.last)
|
||||
if (message != messages.first)
|
||||
SizedBox(
|
||||
height: 8,
|
||||
height: 12,
|
||||
),
|
||||
_MessageItemWrap(
|
||||
_MessageItem(
|
||||
key: GlobalObjectKey(message.id),
|
||||
child: _wrapOffset(
|
||||
_wrapMessage(message),
|
||||
),
|
||||
message: message,
|
||||
onRemove: _handleRemove,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -250,7 +118,6 @@ class MessageManagerState extends State<MessageManager>
|
||||
},
|
||||
),
|
||||
),
|
||||
_floatMessage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -263,22 +130,25 @@ class MessageManagerState extends State<MessageManager>
|
||||
}
|
||||
}
|
||||
|
||||
class _MessageItemWrap extends StatefulWidget {
|
||||
final Widget child;
|
||||
class _MessageItem extends StatefulWidget {
|
||||
final CommonMessage message;
|
||||
final Function(CommonMessage message) onRemove;
|
||||
|
||||
const _MessageItemWrap({
|
||||
const _MessageItem({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.message,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_MessageItemWrap> createState() => _MessageItemWrapState();
|
||||
State<_MessageItem> createState() => _MessageItemState();
|
||||
}
|
||||
|
||||
class _MessageItemWrapState extends State<_MessageItemWrap>
|
||||
class _MessageItemState extends State<_MessageItem>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
Offset _nextOffset = Offset.zero;
|
||||
late Animation<Offset> _offsetAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -287,11 +157,41 @@ class _MessageItemWrapState extends State<_MessageItemWrap>
|
||||
vsync: this,
|
||||
duration: commonDuration * 1.5,
|
||||
);
|
||||
}
|
||||
_offsetAnimation = Tween<Offset>(
|
||||
begin: Offset(-1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
0.0,
|
||||
1,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
));
|
||||
|
||||
transform(Offset offset) async {
|
||||
_nextOffset = offset;
|
||||
await _controller.forward(from: 0);
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
0.0,
|
||||
0.2,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
));
|
||||
|
||||
_controller.forward();
|
||||
|
||||
Future.delayed(
|
||||
widget.message.duration,
|
||||
() async {
|
||||
await _controller.reverse();
|
||||
widget.onRemove(
|
||||
widget.message,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -305,26 +205,30 @@ class _MessageItemWrapState extends State<_MessageItemWrap>
|
||||
return AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
if (_nextOffset == Offset.zero) {
|
||||
return child!;
|
||||
}
|
||||
final offset = Tween(
|
||||
begin: Offset.zero,
|
||||
end: _nextOffset,
|
||||
)
|
||||
.animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _offsetAnimation,
|
||||
child: Material(
|
||||
elevation: _controller.value * 12,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
clipBehavior: Clip.none,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
child: Text(
|
||||
widget.message.text,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 5,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
)
|
||||
.value;
|
||||
return Transform.translate(
|
||||
offset: offset,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ class _TrayContainerState extends State<TrayManager> with TrayListener {
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
render?.active();
|
||||
super.onTrayMenuItemClick(menuItem);
|
||||
}
|
||||
|
||||
@override
|
||||
onTrayIconMouseDown() {
|
||||
window?.show();
|
||||
|
||||
@@ -64,6 +64,18 @@ class _WindowContainerState extends State<WindowManager>
|
||||
super.onWindowClose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
super.onWindowFocus();
|
||||
render?.resume();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
super.onWindowBlur();
|
||||
render?.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onShouldTerminate() async {
|
||||
await globalState.appController.handleExit();
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'common.dart';
|
||||
import 'core.dart';
|
||||
import 'profile.dart';
|
||||
|
||||
typedef DelayMap = Map<String, int?>;
|
||||
typedef DelayMap = Map<String, Map<String, int?>>;
|
||||
|
||||
class AppState with ChangeNotifier {
|
||||
List<NavigationItem> _navigationItems;
|
||||
@@ -122,10 +122,6 @@ class AppState with ChangeNotifier {
|
||||
return selectedMap[firstGroupName] ?? firstGroup.now;
|
||||
}
|
||||
|
||||
int? getDelay(String proxyName) {
|
||||
return _delayMap[getRealProxyName(proxyName)];
|
||||
}
|
||||
|
||||
VersionInfo? get versionInfo => _versionInfo;
|
||||
|
||||
set versionInfo(VersionInfo? value) {
|
||||
@@ -237,15 +233,20 @@ class AppState with ChangeNotifier {
|
||||
}
|
||||
|
||||
set delayMap(DelayMap value) {
|
||||
if (!stringAndIntQMapEquality.equals(_delayMap, value)) {
|
||||
if (_delayMap != value) {
|
||||
_delayMap = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
setDelay(Delay delay) {
|
||||
if (_delayMap[delay.name] != delay.value) {
|
||||
_delayMap = Map.from(_delayMap)..[delay.name] = delay.value;
|
||||
if (_delayMap[delay.url]?[delay.name] != delay.value) {
|
||||
final DelayMap newDelayMap = Map.from(_delayMap);
|
||||
if (newDelayMap[delay.url] == null) {
|
||||
newDelayMap[delay.url] = {};
|
||||
}
|
||||
newDelayMap[delay.url]![delay.name] = delay.value;
|
||||
_delayMap = newDelayMap;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// ignore_for_file: invalid_annotation_target
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -291,6 +293,7 @@ class Group with _$Group {
|
||||
@Default([]) List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
String? testUrl,
|
||||
@Default("") String icon,
|
||||
required String name,
|
||||
}) = _Group;
|
||||
@@ -441,3 +444,24 @@ class Field with _$Field {
|
||||
Validator? validator,
|
||||
}) = _Field;
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
primary,
|
||||
danger,
|
||||
}
|
||||
|
||||
class ActionItemData {
|
||||
const ActionItemData({
|
||||
this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.type,
|
||||
this.iconSize,
|
||||
});
|
||||
|
||||
final double? iconSize;
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final IconData? icon;
|
||||
final ActionType? type;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class AppSetting with _$AppSetting {
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
@Default(defaultDashboardWidgets)
|
||||
List<DashboardWidget> dashboardWidgets,
|
||||
@Default(false) bool onlyProxy,
|
||||
@Default(false) bool onlyStatisticsProxy,
|
||||
@Default(false) bool autoLaunch,
|
||||
@Default(false) bool silentLaunch,
|
||||
@Default(false) bool autoRun,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// 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/core.freezed.dart';
|
||||
|
||||
part 'generated/core.g.dart';
|
||||
|
||||
abstract mixin class AppMessageListener {
|
||||
@@ -16,8 +16,6 @@ abstract mixin class AppMessageListener {
|
||||
|
||||
void onRequest(Connection connection) {}
|
||||
|
||||
void onStarted(String runTime) {}
|
||||
|
||||
void onLoaded(String providerName) {}
|
||||
}
|
||||
|
||||
@@ -25,10 +23,6 @@ abstract mixin class ServiceMessageListener {
|
||||
onProtect(Fd fd) {}
|
||||
|
||||
onProcess(ProcessData process) {}
|
||||
|
||||
onStarted(String runTime) {}
|
||||
|
||||
onLoaded(String providerName) {}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -42,7 +36,6 @@ class CoreState with _$CoreState {
|
||||
required List<String> bypassDomain,
|
||||
required List<String> routeAddress,
|
||||
required bool ipv6,
|
||||
required bool onlyProxy,
|
||||
}) = _CoreState;
|
||||
|
||||
factory CoreState.fromJson(Map<String, Object?> json) =>
|
||||
@@ -76,6 +69,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams {
|
||||
@JsonKey(name: "selected-map") required SelectedMap selectedMap,
|
||||
@JsonKey(name: "override-dns") required bool overrideDns,
|
||||
@JsonKey(name: "test-url") required String testUrl,
|
||||
@JsonKey(name: "only-statistics-proxy") required bool onlyStatisticsProxy,
|
||||
}) = _ConfigExtendedParams;
|
||||
|
||||
factory ConfigExtendedParams.fromJson(Map<String, Object?> json) =>
|
||||
@@ -105,6 +99,17 @@ class ChangeProxyParams with _$ChangeProxyParams {
|
||||
_$ChangeProxyParamsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class UpdateGeoDataParams with _$UpdateGeoDataParams {
|
||||
const factory UpdateGeoDataParams({
|
||||
@JsonKey(name: "geo-type") required String geoType,
|
||||
@JsonKey(name: "geo-name") required String geoName,
|
||||
}) = _UpdateGeoDataParams;
|
||||
|
||||
factory UpdateGeoDataParams.fromJson(Map<String, Object?> json) =>
|
||||
_$UpdateGeoDataParamsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AppMessage with _$AppMessage {
|
||||
const factory AppMessage({
|
||||
@@ -117,20 +122,21 @@ class AppMessage with _$AppMessage {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ServiceMessage with _$ServiceMessage {
|
||||
const factory ServiceMessage({
|
||||
required ServiceMessageType type,
|
||||
class InvokeMessage with _$InvokeMessage {
|
||||
const factory InvokeMessage({
|
||||
required InvokeMessageType type,
|
||||
dynamic data,
|
||||
}) = _ServiceMessage;
|
||||
}) = _InvokeMessage;
|
||||
|
||||
factory ServiceMessage.fromJson(Map<String, Object?> json) =>
|
||||
_$ServiceMessageFromJson(json);
|
||||
factory InvokeMessage.fromJson(Map<String, Object?> json) =>
|
||||
_$InvokeMessageFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Delay with _$Delay {
|
||||
const factory Delay({
|
||||
required String name,
|
||||
required String url,
|
||||
int? value,
|
||||
}) = _Delay;
|
||||
|
||||
@@ -150,7 +156,7 @@ class Now with _$Now {
|
||||
@freezed
|
||||
class ProcessData with _$ProcessData {
|
||||
const factory ProcessData({
|
||||
required int id,
|
||||
required String id,
|
||||
required Metadata metadata,
|
||||
}) = _ProcessData;
|
||||
|
||||
@@ -161,7 +167,7 @@ class ProcessData with _$ProcessData {
|
||||
@freezed
|
||||
class Fd with _$Fd {
|
||||
const factory Fd({
|
||||
required int id,
|
||||
required String id,
|
||||
required int value,
|
||||
}) = _Fd;
|
||||
|
||||
@@ -171,7 +177,7 @@ class Fd with _$Fd {
|
||||
@freezed
|
||||
class ProcessMapItem with _$ProcessMapItem {
|
||||
const factory ProcessMapItem({
|
||||
required int id,
|
||||
required String id,
|
||||
required String value,
|
||||
}) = _ProcessMapItem;
|
||||
|
||||
@@ -241,14 +247,21 @@ class Action with _$Action {
|
||||
const factory Action({
|
||||
required ActionMethod method,
|
||||
required dynamic data,
|
||||
@JsonKey(name: "default-value") required dynamic defaultValue,
|
||||
required String id,
|
||||
}) = _Action;
|
||||
|
||||
factory Action.fromJson(Map<String, Object?> json) => _$ActionFromJson(json);
|
||||
}
|
||||
|
||||
extension ActionExt on Action {
|
||||
String get toJson {
|
||||
return json.encode(this);
|
||||
}
|
||||
@freezed
|
||||
class ActionResult with _$ActionResult {
|
||||
const factory ActionResult({
|
||||
required ActionMethod method,
|
||||
required dynamic data,
|
||||
String? id,
|
||||
}) = _ActionResult;
|
||||
|
||||
factory ActionResult.fromJson(Map<String, Object?> json) =>
|
||||
_$ActionResultFromJson(json);
|
||||
}
|
||||
|
||||
@@ -1998,6 +1998,7 @@ mixin _$Group {
|
||||
List<Proxy> get all => throw _privateConstructorUsedError;
|
||||
String? get now => throw _privateConstructorUsedError;
|
||||
bool? get hidden => throw _privateConstructorUsedError;
|
||||
String? get testUrl => throw _privateConstructorUsedError;
|
||||
String get icon => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
|
||||
@@ -2020,6 +2021,7 @@ abstract class $GroupCopyWith<$Res> {
|
||||
List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
String? testUrl,
|
||||
String icon,
|
||||
String name});
|
||||
}
|
||||
@@ -2043,6 +2045,7 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group>
|
||||
Object? all = null,
|
||||
Object? now = freezed,
|
||||
Object? hidden = freezed,
|
||||
Object? testUrl = freezed,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
}) {
|
||||
@@ -2063,6 +2066,10 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group>
|
||||
? _value.hidden
|
||||
: hidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
testUrl: freezed == testUrl
|
||||
? _value.testUrl
|
||||
: testUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
@@ -2087,6 +2094,7 @@ abstract class _$$GroupImplCopyWith<$Res> implements $GroupCopyWith<$Res> {
|
||||
List<Proxy> all,
|
||||
String? now,
|
||||
bool? hidden,
|
||||
String? testUrl,
|
||||
String icon,
|
||||
String name});
|
||||
}
|
||||
@@ -2108,6 +2116,7 @@ class __$$GroupImplCopyWithImpl<$Res>
|
||||
Object? all = null,
|
||||
Object? now = freezed,
|
||||
Object? hidden = freezed,
|
||||
Object? testUrl = freezed,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
}) {
|
||||
@@ -2128,6 +2137,10 @@ class __$$GroupImplCopyWithImpl<$Res>
|
||||
? _value.hidden
|
||||
: hidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
testUrl: freezed == testUrl
|
||||
? _value.testUrl
|
||||
: testUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
@@ -2148,6 +2161,7 @@ class _$GroupImpl implements _Group {
|
||||
final List<Proxy> all = const [],
|
||||
this.now,
|
||||
this.hidden,
|
||||
this.testUrl,
|
||||
this.icon = "",
|
||||
required this.name})
|
||||
: _all = all;
|
||||
@@ -2171,6 +2185,8 @@ class _$GroupImpl implements _Group {
|
||||
@override
|
||||
final bool? hidden;
|
||||
@override
|
||||
final String? testUrl;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String icon;
|
||||
@override
|
||||
@@ -2178,7 +2194,7 @@ class _$GroupImpl implements _Group {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Group(type: $type, all: $all, now: $now, hidden: $hidden, icon: $icon, name: $name)';
|
||||
return 'Group(type: $type, all: $all, now: $now, hidden: $hidden, testUrl: $testUrl, icon: $icon, name: $name)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2190,14 +2206,22 @@ class _$GroupImpl implements _Group {
|
||||
const DeepCollectionEquality().equals(other._all, _all) &&
|
||||
(identical(other.now, now) || other.now == now) &&
|
||||
(identical(other.hidden, hidden) || other.hidden == hidden) &&
|
||||
(identical(other.testUrl, testUrl) || other.testUrl == testUrl) &&
|
||||
(identical(other.icon, icon) || other.icon == icon) &&
|
||||
(identical(other.name, name) || other.name == name));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, type,
|
||||
const DeepCollectionEquality().hash(_all), now, hidden, icon, name);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
type,
|
||||
const DeepCollectionEquality().hash(_all),
|
||||
now,
|
||||
hidden,
|
||||
testUrl,
|
||||
icon,
|
||||
name);
|
||||
|
||||
/// Create a copy of Group
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -2221,6 +2245,7 @@ abstract class _Group implements Group {
|
||||
final List<Proxy> all,
|
||||
final String? now,
|
||||
final bool? hidden,
|
||||
final String? testUrl,
|
||||
final String icon,
|
||||
required final String name}) = _$GroupImpl;
|
||||
|
||||
@@ -2235,6 +2260,8 @@ abstract class _Group implements Group {
|
||||
@override
|
||||
bool? get hidden;
|
||||
@override
|
||||
String? get testUrl;
|
||||
@override
|
||||
String get icon;
|
||||
@override
|
||||
String get name;
|
||||
|
||||
@@ -161,6 +161,7 @@ _$GroupImpl _$$GroupImplFromJson(Map<String, dynamic> json) => _$GroupImpl(
|
||||
const [],
|
||||
now: json['now'] as String?,
|
||||
hidden: json['hidden'] as bool?,
|
||||
testUrl: json['testUrl'] as String?,
|
||||
icon: json['icon'] as String? ?? "",
|
||||
name: json['name'] as String,
|
||||
);
|
||||
@@ -171,6 +172,7 @@ Map<String, dynamic> _$$GroupImplToJson(_$GroupImpl instance) =>
|
||||
'all': instance.all,
|
||||
'now': instance.now,
|
||||
'hidden': instance.hidden,
|
||||
'testUrl': instance.testUrl,
|
||||
'icon': instance.icon,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ mixin _$AppSetting {
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
List<DashboardWidget> get dashboardWidgets =>
|
||||
throw _privateConstructorUsedError;
|
||||
bool get onlyProxy => throw _privateConstructorUsedError;
|
||||
bool get onlyStatisticsProxy => throw _privateConstructorUsedError;
|
||||
bool get autoLaunch => throw _privateConstructorUsedError;
|
||||
bool get silentLaunch => throw _privateConstructorUsedError;
|
||||
bool get autoRun => throw _privateConstructorUsedError;
|
||||
@@ -58,7 +58,7 @@ abstract class $AppSettingCopyWith<$Res> {
|
||||
{String? locale,
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
List<DashboardWidget> dashboardWidgets,
|
||||
bool onlyProxy,
|
||||
bool onlyStatisticsProxy,
|
||||
bool autoLaunch,
|
||||
bool silentLaunch,
|
||||
bool autoRun,
|
||||
@@ -90,7 +90,7 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
|
||||
$Res call({
|
||||
Object? locale = freezed,
|
||||
Object? dashboardWidgets = null,
|
||||
Object? onlyProxy = null,
|
||||
Object? onlyStatisticsProxy = null,
|
||||
Object? autoLaunch = null,
|
||||
Object? silentLaunch = null,
|
||||
Object? autoRun = null,
|
||||
@@ -113,9 +113,9 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
|
||||
? _value.dashboardWidgets
|
||||
: dashboardWidgets // ignore: cast_nullable_to_non_nullable
|
||||
as List<DashboardWidget>,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
onlyStatisticsProxy: null == onlyStatisticsProxy
|
||||
? _value.onlyStatisticsProxy
|
||||
: onlyStatisticsProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
autoLaunch: null == autoLaunch
|
||||
? _value.autoLaunch
|
||||
@@ -181,7 +181,7 @@ abstract class _$$AppSettingImplCopyWith<$Res>
|
||||
{String? locale,
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
List<DashboardWidget> dashboardWidgets,
|
||||
bool onlyProxy,
|
||||
bool onlyStatisticsProxy,
|
||||
bool autoLaunch,
|
||||
bool silentLaunch,
|
||||
bool autoRun,
|
||||
@@ -211,7 +211,7 @@ class __$$AppSettingImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? locale = freezed,
|
||||
Object? dashboardWidgets = null,
|
||||
Object? onlyProxy = null,
|
||||
Object? onlyStatisticsProxy = null,
|
||||
Object? autoLaunch = null,
|
||||
Object? silentLaunch = null,
|
||||
Object? autoRun = null,
|
||||
@@ -234,9 +234,9 @@ class __$$AppSettingImplCopyWithImpl<$Res>
|
||||
? _value._dashboardWidgets
|
||||
: dashboardWidgets // ignore: cast_nullable_to_non_nullable
|
||||
as List<DashboardWidget>,
|
||||
onlyProxy: null == onlyProxy
|
||||
? _value.onlyProxy
|
||||
: onlyProxy // ignore: cast_nullable_to_non_nullable
|
||||
onlyStatisticsProxy: null == onlyStatisticsProxy
|
||||
? _value.onlyStatisticsProxy
|
||||
: onlyStatisticsProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
autoLaunch: null == autoLaunch
|
||||
? _value.autoLaunch
|
||||
@@ -297,7 +297,7 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
{this.locale,
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
final List<DashboardWidget> dashboardWidgets = defaultDashboardWidgets,
|
||||
this.onlyProxy = false,
|
||||
this.onlyStatisticsProxy = false,
|
||||
this.autoLaunch = false,
|
||||
this.silentLaunch = false,
|
||||
this.autoRun = false,
|
||||
@@ -329,7 +329,7 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool onlyProxy;
|
||||
final bool onlyStatisticsProxy;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool autoLaunch;
|
||||
@@ -369,7 +369,7 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSetting(locale: $locale, dashboardWidgets: $dashboardWidgets, 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)';
|
||||
return 'AppSetting(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, 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
|
||||
@@ -380,8 +380,8 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
(identical(other.locale, locale) || other.locale == locale) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._dashboardWidgets, _dashboardWidgets) &&
|
||||
(identical(other.onlyProxy, onlyProxy) ||
|
||||
other.onlyProxy == onlyProxy) &&
|
||||
(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) ||
|
||||
other.onlyStatisticsProxy == onlyStatisticsProxy) &&
|
||||
(identical(other.autoLaunch, autoLaunch) ||
|
||||
other.autoLaunch == autoLaunch) &&
|
||||
(identical(other.silentLaunch, silentLaunch) ||
|
||||
@@ -411,7 +411,7 @@ class _$AppSettingImpl implements _AppSetting {
|
||||
runtimeType,
|
||||
locale,
|
||||
const DeepCollectionEquality().hash(_dashboardWidgets),
|
||||
onlyProxy,
|
||||
onlyStatisticsProxy,
|
||||
autoLaunch,
|
||||
silentLaunch,
|
||||
autoRun,
|
||||
@@ -446,7 +446,7 @@ abstract class _AppSetting implements AppSetting {
|
||||
{final String? locale,
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
final List<DashboardWidget> dashboardWidgets,
|
||||
final bool onlyProxy,
|
||||
final bool onlyStatisticsProxy,
|
||||
final bool autoLaunch,
|
||||
final bool silentLaunch,
|
||||
final bool autoRun,
|
||||
@@ -469,7 +469,7 @@ abstract class _AppSetting implements AppSetting {
|
||||
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
|
||||
List<DashboardWidget> get dashboardWidgets;
|
||||
@override
|
||||
bool get onlyProxy;
|
||||
bool get onlyStatisticsProxy;
|
||||
@override
|
||||
bool get autoLaunch;
|
||||
@override
|
||||
|
||||
@@ -57,7 +57,7 @@ _$AppSettingImpl _$$AppSettingImplFromJson(Map<String, dynamic> json) =>
|
||||
dashboardWidgets: json['dashboardWidgets'] == null
|
||||
? defaultDashboardWidgets
|
||||
: dashboardWidgetsRealFormJson(json['dashboardWidgets'] as List?),
|
||||
onlyProxy: json['onlyProxy'] as bool? ?? false,
|
||||
onlyStatisticsProxy: json['onlyStatisticsProxy'] as bool? ?? false,
|
||||
autoLaunch: json['autoLaunch'] as bool? ?? false,
|
||||
silentLaunch: json['silentLaunch'] as bool? ?? false,
|
||||
autoRun: json['autoRun'] as bool? ?? false,
|
||||
@@ -78,7 +78,7 @@ Map<String, dynamic> _$$AppSettingImplToJson(_$AppSettingImpl instance) =>
|
||||
'dashboardWidgets': instance.dashboardWidgets
|
||||
.map((e) => _$DashboardWidgetEnumMap[e]!)
|
||||
.toList(),
|
||||
'onlyProxy': instance.onlyProxy,
|
||||
'onlyStatisticsProxy': instance.onlyStatisticsProxy,
|
||||
'autoLaunch': instance.autoLaunch,
|
||||
'silentLaunch': instance.silentLaunch,
|
||||
'autoRun': instance.autoRun,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,6 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map<String, dynamic> json) =>
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
ipv6: json['ipv6'] as bool,
|
||||
onlyProxy: json['onlyProxy'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
|
||||
@@ -36,7 +35,6 @@ Map<String, dynamic> _$$CoreStateImplToJson(_$CoreStateImpl instance) =>
|
||||
'bypassDomain': instance.bypassDomain,
|
||||
'routeAddress': instance.routeAddress,
|
||||
'ipv6': instance.ipv6,
|
||||
'onlyProxy': instance.onlyProxy,
|
||||
};
|
||||
|
||||
_$AndroidVpnOptionsImpl _$$AndroidVpnOptionsImplFromJson(
|
||||
@@ -84,6 +82,7 @@ _$ConfigExtendedParamsImpl _$$ConfigExtendedParamsImplFromJson(
|
||||
selectedMap: Map<String, String>.from(json['selected-map'] as Map),
|
||||
overrideDns: json['override-dns'] as bool,
|
||||
testUrl: json['test-url'] as String,
|
||||
onlyStatisticsProxy: json['only-statistics-proxy'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ConfigExtendedParamsImplToJson(
|
||||
@@ -94,6 +93,7 @@ Map<String, dynamic> _$$ConfigExtendedParamsImplToJson(
|
||||
'selected-map': instance.selectedMap,
|
||||
'override-dns': instance.overrideDns,
|
||||
'test-url': instance.testUrl,
|
||||
'only-statistics-proxy': instance.onlyStatisticsProxy,
|
||||
};
|
||||
|
||||
_$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson(
|
||||
@@ -127,6 +127,20 @@ Map<String, dynamic> _$$ChangeProxyParamsImplToJson(
|
||||
'proxy-name': instance.proxyName,
|
||||
};
|
||||
|
||||
_$UpdateGeoDataParamsImpl _$$UpdateGeoDataParamsImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$UpdateGeoDataParamsImpl(
|
||||
geoType: json['geo-type'] as String,
|
||||
geoName: json['geo-name'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$UpdateGeoDataParamsImplToJson(
|
||||
_$UpdateGeoDataParamsImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'geo-type': instance.geoType,
|
||||
'geo-name': instance.geoName,
|
||||
};
|
||||
|
||||
_$AppMessageImpl _$$AppMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$AppMessageImpl(
|
||||
type: $enumDecode(_$AppMessageTypeEnumMap, json['type']),
|
||||
@@ -143,38 +157,36 @@ const _$AppMessageTypeEnumMap = {
|
||||
AppMessageType.log: 'log',
|
||||
AppMessageType.delay: 'delay',
|
||||
AppMessageType.request: 'request',
|
||||
AppMessageType.started: 'started',
|
||||
AppMessageType.loaded: 'loaded',
|
||||
};
|
||||
|
||||
_$ServiceMessageImpl _$$ServiceMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ServiceMessageImpl(
|
||||
type: $enumDecode(_$ServiceMessageTypeEnumMap, json['type']),
|
||||
_$InvokeMessageImpl _$$InvokeMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$InvokeMessageImpl(
|
||||
type: $enumDecode(_$InvokeMessageTypeEnumMap, json['type']),
|
||||
data: json['data'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ServiceMessageImplToJson(
|
||||
_$ServiceMessageImpl instance) =>
|
||||
Map<String, dynamic> _$$InvokeMessageImplToJson(_$InvokeMessageImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$ServiceMessageTypeEnumMap[instance.type]!,
|
||||
'type': _$InvokeMessageTypeEnumMap[instance.type]!,
|
||||
'data': instance.data,
|
||||
};
|
||||
|
||||
const _$ServiceMessageTypeEnumMap = {
|
||||
ServiceMessageType.protect: 'protect',
|
||||
ServiceMessageType.process: 'process',
|
||||
ServiceMessageType.started: 'started',
|
||||
ServiceMessageType.loaded: 'loaded',
|
||||
const _$InvokeMessageTypeEnumMap = {
|
||||
InvokeMessageType.protect: 'protect',
|
||||
InvokeMessageType.process: 'process',
|
||||
};
|
||||
|
||||
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(
|
||||
name: json['name'] as String,
|
||||
url: json['url'] as String,
|
||||
value: (json['value'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$DelayImplToJson(_$DelayImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'url': instance.url,
|
||||
'value': instance.value,
|
||||
};
|
||||
|
||||
@@ -190,7 +202,7 @@ Map<String, dynamic> _$$NowImplToJson(_$NowImpl instance) => <String, dynamic>{
|
||||
|
||||
_$ProcessDataImpl _$$ProcessDataImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ProcessDataImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
id: json['id'] as String,
|
||||
metadata: Metadata.fromJson(json['metadata'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
@@ -201,7 +213,7 @@ Map<String, dynamic> _$$ProcessDataImplToJson(_$ProcessDataImpl instance) =>
|
||||
};
|
||||
|
||||
_$FdImpl _$$FdImplFromJson(Map<String, dynamic> json) => _$FdImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
id: json['id'] as String,
|
||||
value: (json['value'] as num).toInt(),
|
||||
);
|
||||
|
||||
@@ -212,7 +224,7 @@ Map<String, dynamic> _$$FdImplToJson(_$FdImpl instance) => <String, dynamic>{
|
||||
|
||||
_$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ProcessMapItemImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
id: json['id'] as String,
|
||||
value: json['value'] as String,
|
||||
);
|
||||
|
||||
@@ -293,6 +305,7 @@ Map<String, dynamic> _$$TunPropsImplToJson(_$TunPropsImpl instance) =>
|
||||
_$ActionImpl _$$ActionImplFromJson(Map<String, dynamic> json) => _$ActionImpl(
|
||||
method: $enumDecode(_$ActionMethodEnumMap, json['method']),
|
||||
data: json['data'],
|
||||
defaultValue: json['default-value'],
|
||||
id: json['id'] as String,
|
||||
);
|
||||
|
||||
@@ -300,6 +313,7 @@ Map<String, dynamic> _$$ActionImplToJson(_$ActionImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'method': _$ActionMethodEnumMap[instance.method]!,
|
||||
'data': instance.data,
|
||||
'default-value': instance.defaultValue,
|
||||
'id': instance.id,
|
||||
};
|
||||
|
||||
@@ -331,4 +345,27 @@ const _$ActionMethodEnumMap = {
|
||||
ActionMethod.stopListener: 'stopListener',
|
||||
ActionMethod.getCountryCode: 'getCountryCode',
|
||||
ActionMethod.getMemory: 'getMemory',
|
||||
ActionMethod.setFdMap: 'setFdMap',
|
||||
ActionMethod.setProcessMap: 'setProcessMap',
|
||||
ActionMethod.setState: 'setState',
|
||||
ActionMethod.startTun: 'startTun',
|
||||
ActionMethod.stopTun: 'stopTun',
|
||||
ActionMethod.getRunTime: 'getRunTime',
|
||||
ActionMethod.updateDns: 'updateDns',
|
||||
ActionMethod.getAndroidVpnOptions: 'getAndroidVpnOptions',
|
||||
ActionMethod.getCurrentProfileName: 'getCurrentProfileName',
|
||||
};
|
||||
|
||||
_$ActionResultImpl _$$ActionResultImplFromJson(Map<String, dynamic> json) =>
|
||||
_$ActionResultImpl(
|
||||
method: $enumDecode(_$ActionMethodEnumMap, json['method']),
|
||||
data: json['data'],
|
||||
id: json['id'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ActionResultImplToJson(_$ActionResultImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'method': _$ActionMethodEnumMap[instance.method]!,
|
||||
'data': instance.data,
|
||||
'id': instance.id,
|
||||
};
|
||||
|
||||
@@ -2298,6 +2298,7 @@ abstract class _ProxiesListSelectorState implements ProxiesListSelectorState {
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ProxyGroupSelectorState {
|
||||
String? get testUrl => throw _privateConstructorUsedError;
|
||||
ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError;
|
||||
ProxyCardType get proxyCardType => throw _privateConstructorUsedError;
|
||||
num get sortNum => throw _privateConstructorUsedError;
|
||||
@@ -2319,7 +2320,8 @@ abstract class $ProxyGroupSelectorStateCopyWith<$Res> {
|
||||
_$ProxyGroupSelectorStateCopyWithImpl<$Res, ProxyGroupSelectorState>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{ProxiesSortType proxiesSortType,
|
||||
{String? testUrl,
|
||||
ProxiesSortType proxiesSortType,
|
||||
ProxyCardType proxyCardType,
|
||||
num sortNum,
|
||||
GroupType groupType,
|
||||
@@ -2343,6 +2345,7 @@ class _$ProxyGroupSelectorStateCopyWithImpl<$Res,
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? testUrl = freezed,
|
||||
Object? proxiesSortType = null,
|
||||
Object? proxyCardType = null,
|
||||
Object? sortNum = null,
|
||||
@@ -2351,6 +2354,10 @@ class _$ProxyGroupSelectorStateCopyWithImpl<$Res,
|
||||
Object? columns = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
testUrl: freezed == testUrl
|
||||
? _value.testUrl
|
||||
: testUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
proxiesSortType: null == proxiesSortType
|
||||
? _value.proxiesSortType
|
||||
: proxiesSortType // ignore: cast_nullable_to_non_nullable
|
||||
@@ -2389,7 +2396,8 @@ abstract class _$$ProxyGroupSelectorStateImplCopyWith<$Res>
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{ProxiesSortType proxiesSortType,
|
||||
{String? testUrl,
|
||||
ProxiesSortType proxiesSortType,
|
||||
ProxyCardType proxyCardType,
|
||||
num sortNum,
|
||||
GroupType groupType,
|
||||
@@ -2412,6 +2420,7 @@ class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? testUrl = freezed,
|
||||
Object? proxiesSortType = null,
|
||||
Object? proxyCardType = null,
|
||||
Object? sortNum = null,
|
||||
@@ -2420,6 +2429,10 @@ class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>
|
||||
Object? columns = null,
|
||||
}) {
|
||||
return _then(_$ProxyGroupSelectorStateImpl(
|
||||
testUrl: freezed == testUrl
|
||||
? _value.testUrl
|
||||
: testUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
proxiesSortType: null == proxiesSortType
|
||||
? _value.proxiesSortType
|
||||
: proxiesSortType // ignore: cast_nullable_to_non_nullable
|
||||
@@ -2452,7 +2465,8 @@ class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>
|
||||
|
||||
class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
const _$ProxyGroupSelectorStateImpl(
|
||||
{required this.proxiesSortType,
|
||||
{required this.testUrl,
|
||||
required this.proxiesSortType,
|
||||
required this.proxyCardType,
|
||||
required this.sortNum,
|
||||
required this.groupType,
|
||||
@@ -2460,6 +2474,8 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
required this.columns})
|
||||
: _proxies = proxies;
|
||||
|
||||
@override
|
||||
final String? testUrl;
|
||||
@override
|
||||
final ProxiesSortType proxiesSortType;
|
||||
@override
|
||||
@@ -2481,7 +2497,7 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProxyGroupSelectorState(proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, groupType: $groupType, proxies: $proxies, columns: $columns)';
|
||||
return 'ProxyGroupSelectorState(testUrl: $testUrl, proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, groupType: $groupType, proxies: $proxies, columns: $columns)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2489,6 +2505,7 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ProxyGroupSelectorStateImpl &&
|
||||
(identical(other.testUrl, testUrl) || other.testUrl == testUrl) &&
|
||||
(identical(other.proxiesSortType, proxiesSortType) ||
|
||||
other.proxiesSortType == proxiesSortType) &&
|
||||
(identical(other.proxyCardType, proxyCardType) ||
|
||||
@@ -2503,6 +2520,7 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
testUrl,
|
||||
proxiesSortType,
|
||||
proxyCardType,
|
||||
sortNum,
|
||||
@@ -2522,13 +2540,16 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
|
||||
|
||||
abstract class _ProxyGroupSelectorState implements ProxyGroupSelectorState {
|
||||
const factory _ProxyGroupSelectorState(
|
||||
{required final ProxiesSortType proxiesSortType,
|
||||
{required final String? testUrl,
|
||||
required final ProxiesSortType proxiesSortType,
|
||||
required final ProxyCardType proxyCardType,
|
||||
required final num sortNum,
|
||||
required final GroupType groupType,
|
||||
required final List<Proxy> proxies,
|
||||
required final int columns}) = _$ProxyGroupSelectorStateImpl;
|
||||
|
||||
@override
|
||||
String? get testUrl;
|
||||
@override
|
||||
ProxiesSortType get proxiesSortType;
|
||||
@override
|
||||
|
||||
@@ -123,6 +123,7 @@ class ProxiesListSelectorState with _$ProxiesListSelectorState {
|
||||
@freezed
|
||||
class ProxyGroupSelectorState with _$ProxyGroupSelectorState {
|
||||
const factory ProxyGroupSelectorState({
|
||||
required String? testUrl,
|
||||
required ProxiesSortType proxiesSortType,
|
||||
required ProxyCardType proxyCardType,
|
||||
required num sortNum,
|
||||
|
||||
259
lib/pages/editor.dart
Normal file
259
lib/pages/editor.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:re_editor/re_editor.dart';
|
||||
import 'package:re_highlight/languages/yaml.dart';
|
||||
import 'package:re_highlight/styles/atom-one-light.dart';
|
||||
|
||||
import '../models/common.dart';
|
||||
|
||||
typedef EditingValueChangeBuilder = Widget Function(CodeLineEditingValue value);
|
||||
|
||||
class EditorPage extends StatefulWidget {
|
||||
final String title;
|
||||
final String content;
|
||||
final Function(BuildContext context, String text)? onSave;
|
||||
final Future<bool> Function(BuildContext context, String text)? onPop;
|
||||
|
||||
const EditorPage({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.onSave,
|
||||
this.onPop,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EditorPage> createState() => _EditorPageState();
|
||||
}
|
||||
|
||||
class _EditorPageState extends State<EditorPage> {
|
||||
late CodeLineEditingController _controller;
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CodeLineEditingController.fromText(widget.content);
|
||||
if (system.isDesktop) {
|
||||
return;
|
||||
}
|
||||
_focusNode.onKeyEvent = ((_, event) {
|
||||
final keys = HardwareKeyboard.instance.logicalKeysPressed;
|
||||
final key = event.logicalKey;
|
||||
if (!keys.contains(key)) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.arrowUp) {
|
||||
_controller.moveCursor(AxisDirection.up);
|
||||
return KeyEventResult.handled;
|
||||
} else if (key == LogicalKeyboardKey.arrowDown) {
|
||||
_controller.moveCursor(AxisDirection.down);
|
||||
return KeyEventResult.handled;
|
||||
} else if (key == LogicalKeyboardKey.arrowLeft) {
|
||||
_controller.selection.endIndex;
|
||||
_controller.moveCursor(AxisDirection.left);
|
||||
return KeyEventResult.handled;
|
||||
} else if (key == LogicalKeyboardKey.arrowRight) {
|
||||
_controller.moveCursor(AxisDirection.right);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _wrapController(EditingValueChangeBuilder builder) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _controller,
|
||||
builder: (_, value, ___) {
|
||||
return builder(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
if (widget.onPop != null) {
|
||||
final res = await widget.onPop!(context, _controller.text);
|
||||
if (res && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: CommonScaffold(
|
||||
actions: [
|
||||
_wrapController(
|
||||
(value) => IconButton(
|
||||
onPressed: _controller.canUndo ? _controller.undo : null,
|
||||
icon: const Icon(Icons.undo),
|
||||
),
|
||||
),
|
||||
_wrapController(
|
||||
(value) => IconButton(
|
||||
onPressed: _controller.canRedo ? _controller.redo : null,
|
||||
icon: const Icon(Icons.redo),
|
||||
),
|
||||
),
|
||||
if (widget.onSave != null)
|
||||
_wrapController(
|
||||
(value) => IconButton(
|
||||
onPressed: _controller.text == widget.content
|
||||
? null
|
||||
: () {
|
||||
widget.onSave!(context, _controller.text);
|
||||
},
|
||||
icon: const Icon(Icons.save_sharp),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: CodeEditor(
|
||||
focusNode: _focusNode,
|
||||
scrollbarBuilder: (context, child, details) {
|
||||
return Scrollbar(
|
||||
controller: details.controller,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(2),
|
||||
interactive: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
toolbarController: ContextMenuControllerImpl(),
|
||||
indicatorBuilder: (
|
||||
context,
|
||||
editingController,
|
||||
chunkController,
|
||||
notifier,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
DefaultCodeLineNumber(
|
||||
controller: editingController,
|
||||
notifier: notifier,
|
||||
),
|
||||
DefaultCodeChunkIndicator(
|
||||
width: 20,
|
||||
controller: chunkController,
|
||||
notifier: notifier,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
shortcutsActivatorsBuilder: DefaultCodeShortcutsActivatorsBuilder(),
|
||||
controller: _controller,
|
||||
style: CodeEditorStyle(
|
||||
fontSize: 14,
|
||||
codeTheme: CodeHighlightTheme(
|
||||
languages: {
|
||||
'yaml': CodeHighlightThemeMode(
|
||||
mode: langYaml,
|
||||
)
|
||||
},
|
||||
theme: atomOneLightTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: widget.title,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuControllerImpl implements SelectionToolbarController {
|
||||
OverlayEntry? _overlayEntry;
|
||||
bool _isFirstRender = true;
|
||||
|
||||
_removeOverLayEntry() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
_isFirstRender = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void hide(BuildContext context) {
|
||||
_removeOverLayEntry();
|
||||
}
|
||||
|
||||
@override
|
||||
void show({
|
||||
required context,
|
||||
required controller,
|
||||
required anchors,
|
||||
renderRect,
|
||||
required layerLink,
|
||||
required ValueNotifier<bool> visibility,
|
||||
}) {
|
||||
_removeOverLayEntry();
|
||||
_overlayEntry ??= OverlayEntry(
|
||||
builder: (context) => CodeEditorTapRegion(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller,
|
||||
builder: (_, __, child) {
|
||||
final isNotEmpty = controller.selectedText.isNotEmpty;
|
||||
final isAllSelected = controller.isAllSelected;
|
||||
final hasSelected = controller.selectedText.isNotEmpty;
|
||||
List<ActionItemData> menus = [
|
||||
if (isNotEmpty)
|
||||
ActionItemData(
|
||||
label: appLocalizations.copy,
|
||||
onPressed: controller.copy,
|
||||
),
|
||||
ActionItemData(
|
||||
label: appLocalizations.paste,
|
||||
onPressed: controller.paste,
|
||||
),
|
||||
if (isNotEmpty)
|
||||
ActionItemData(
|
||||
label: appLocalizations.cut,
|
||||
onPressed: controller.cut,
|
||||
),
|
||||
if (hasSelected && !isAllSelected)
|
||||
ActionItemData(
|
||||
label: appLocalizations.selectAll,
|
||||
onPressed: controller.selectAll,
|
||||
),
|
||||
];
|
||||
if (_isFirstRender) {
|
||||
_isFirstRender = false;
|
||||
} else if (controller.selectedText.isEmpty) {
|
||||
_removeOverLayEntry();
|
||||
}
|
||||
return TextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor ?? Offset.zero,
|
||||
children: menus.asMap().entries.map(
|
||||
(MapEntry<int, ActionItemData> entry) {
|
||||
return TextSelectionToolbarTextButton(
|
||||
padding: TextSelectionToolbarTextButton.getPadding(
|
||||
entry.key,
|
||||
menus.length,
|
||||
),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
onPressed: () {
|
||||
entry.value.onPressed();
|
||||
},
|
||||
child: Text(entry.value.label),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export 'home.dart';
|
||||
export 'scan.dart';
|
||||
export 'scan.dart';
|
||||
export 'editor.dart';
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../clash/lib.dart';
|
||||
|
||||
class Service {
|
||||
static Service? _instance;
|
||||
late MethodChannel methodChannel;
|
||||
@@ -24,7 +28,17 @@ class Service {
|
||||
Future<bool?> destroy() async {
|
||||
return await methodChannel.invokeMethod<bool>("destroy");
|
||||
}
|
||||
|
||||
Future<bool?> startVpn() async {
|
||||
final options = await clashLib?.getAndroidVpnOptions();
|
||||
return await methodChannel.invokeMethod<bool>("startVpn", {
|
||||
'data': json.encode(options),
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool?> stopVpn() async {
|
||||
return await methodChannel.invokeMethod<bool>("stopVpn");
|
||||
}
|
||||
}
|
||||
|
||||
final service =
|
||||
Platform.isAndroid ? Service() : null;
|
||||
Service? get service => Platform.isAndroid && !globalState.isService ? Service() : null;
|
||||
|
||||
@@ -15,7 +15,6 @@ abstract mixin class TileListener {
|
||||
}
|
||||
|
||||
class Tile {
|
||||
StreamSubscription? subscription;
|
||||
|
||||
final MethodChannel _channel = const MethodChannel('tile');
|
||||
|
||||
|
||||
@@ -1,35 +1,46 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
abstract mixin class VpnListener {
|
||||
void onStarted(int fd) {}
|
||||
|
||||
void onDnsChanged(String dns) {}
|
||||
}
|
||||
|
||||
class Vpn {
|
||||
static Vpn? _instance;
|
||||
late MethodChannel methodChannel;
|
||||
ReceivePort? receiver;
|
||||
ServiceMessageListener? _serviceMessageHandler;
|
||||
FutureOr<String> Function()? handleGetStartForegroundParams;
|
||||
|
||||
Vpn._internal() {
|
||||
methodChannel = const MethodChannel("vpn");
|
||||
methodChannel.setMethodCallHandler((call) async {
|
||||
switch (call.method) {
|
||||
case "started":
|
||||
final fd = call.arguments as int;
|
||||
onStarted(fd);
|
||||
break;
|
||||
case "gc":
|
||||
clashCore.requestGc();
|
||||
case "dnsChanged":
|
||||
final dns = call.arguments as String;
|
||||
clashLib?.updateDns(dns);
|
||||
case "getStartForegroundParams":
|
||||
if (handleGetStartForegroundParams != null) {
|
||||
return await handleGetStartForegroundParams!();
|
||||
}
|
||||
return "";
|
||||
default:
|
||||
throw MissingPluginException();
|
||||
for (final VpnListener listener in _listeners) {
|
||||
switch (call.method) {
|
||||
case "started":
|
||||
final fd = call.arguments as int;
|
||||
listener.onStarted(fd);
|
||||
break;
|
||||
case "dnsChanged":
|
||||
final dns = call.arguments as String;
|
||||
listener.onDnsChanged(dns);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -39,14 +50,15 @@ class Vpn {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<bool?> startVpn() async {
|
||||
final options = clashLib?.getAndroidVpnOptions();
|
||||
final ObserverList<VpnListener> _listeners = ObserverList<VpnListener>();
|
||||
|
||||
Future<bool?> start(AndroidVpnOptions options) async {
|
||||
return await methodChannel.invokeMethod<bool>("start", {
|
||||
'data': json.encode(options),
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool?> stopVpn() async {
|
||||
Future<bool?> stop() async {
|
||||
return await methodChannel.invokeMethod<bool>("stop");
|
||||
}
|
||||
|
||||
@@ -60,45 +72,13 @@ class Vpn {
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool?> startForeground({
|
||||
required String title,
|
||||
required String content,
|
||||
}) async {
|
||||
return await methodChannel.invokeMethod<bool?>("startForeground", {
|
||||
'title': title,
|
||||
'content': content,
|
||||
});
|
||||
void addListener(VpnListener listener) {
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
onStarted(int fd) {
|
||||
if (receiver != null) {
|
||||
receiver!.close();
|
||||
receiver == null;
|
||||
}
|
||||
receiver = ReceivePort();
|
||||
receiver!.listen((message) {
|
||||
_handleServiceMessage(message);
|
||||
});
|
||||
clashLib?.startTun(fd, receiver!.sendPort.nativePort);
|
||||
}
|
||||
|
||||
setServiceMessageHandler(ServiceMessageListener serviceMessageListener) {
|
||||
_serviceMessageHandler = serviceMessageListener;
|
||||
}
|
||||
|
||||
_handleServiceMessage(String message) {
|
||||
final m = ServiceMessage.fromJson(json.decode(message));
|
||||
switch (m.type) {
|
||||
case ServiceMessageType.protect:
|
||||
_serviceMessageHandler?.onProtect(Fd.fromJson(m.data));
|
||||
case ServiceMessageType.process:
|
||||
_serviceMessageHandler?.onProcess(ProcessData.fromJson(m.data));
|
||||
case ServiceMessageType.started:
|
||||
_serviceMessageHandler?.onStarted(m.data);
|
||||
case ServiceMessageType.loaded:
|
||||
_serviceMessageHandler?.onLoaded(m.data);
|
||||
}
|
||||
void removeListener(VpnListener listener) {
|
||||
_listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
final vpn = Platform.isAndroid ? Vpn() : null;
|
||||
Vpn? get vpn => globalState.isService ? Vpn() : null;
|
||||
|
||||
156
lib/state.dart
156
lib/state.dart
@@ -5,7 +5,6 @@ import 'package:animations/animations.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
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:package_info_plus/package_info_plus.dart';
|
||||
@@ -15,37 +14,49 @@ import 'common/common.dart';
|
||||
import 'controller.dart';
|
||||
import 'models/models.dart';
|
||||
|
||||
typedef UpdateTasks = List<FutureOr Function()>;
|
||||
|
||||
class GlobalState {
|
||||
bool isService = false;
|
||||
Timer? timer;
|
||||
Timer? groupsUpdateTimer;
|
||||
var isVpnService = false;
|
||||
late PackageInfo packageInfo;
|
||||
Function? updateCurrentDelayDebounce;
|
||||
PageController? pageController;
|
||||
late Measure measure;
|
||||
DateTime? startTime;
|
||||
UpdateTasks tasks = [];
|
||||
final safeMessageOffsetNotifier = ValueNotifier(Offset.zero);
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
late AppController appController;
|
||||
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
|
||||
List<Function> updateFunctionLists = [];
|
||||
bool lastTunEnable = false;
|
||||
int? lastProfileModified;
|
||||
|
||||
bool get isStart => startTime != null && startTime!.isBeforeNow;
|
||||
|
||||
startListenUpdate() {
|
||||
startUpdateTasks([UpdateTasks? tasks]) async {
|
||||
if (timer != null && timer!.isActive == true) return;
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
for (final function in updateFunctionLists) {
|
||||
function();
|
||||
}
|
||||
if (tasks != null) {
|
||||
this.tasks = tasks;
|
||||
}
|
||||
await executorUpdateTask();
|
||||
timer = Timer(const Duration(seconds: 1), () async {
|
||||
startUpdateTasks();
|
||||
});
|
||||
}
|
||||
|
||||
stopListenUpdate() {
|
||||
executorUpdateTask() async {
|
||||
if (timer != null && timer!.isActive == true) return;
|
||||
for (final task in tasks) {
|
||||
await task();
|
||||
}
|
||||
}
|
||||
|
||||
stopUpdateTasks() {
|
||||
if (timer == null || timer?.isActive == false) return;
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
Future<void> initCore({
|
||||
@@ -100,33 +111,18 @@ class GlobalState {
|
||||
clashCore.stopLog();
|
||||
}
|
||||
final res = await clashCore.updateConfig(
|
||||
UpdateConfigParams(
|
||||
profileId: config.currentProfileId ?? "",
|
||||
config: useClashConfig,
|
||||
params: ConfigExtendedParams(
|
||||
isPatch: isPatch,
|
||||
isCompatible: true,
|
||||
selectedMap: config.currentSelectedMap,
|
||||
overrideDns: config.overrideDns,
|
||||
testUrl: config.appSetting.testUrl,
|
||||
),
|
||||
),
|
||||
getUpdateConfigParams(config, clashConfig, isPatch),
|
||||
);
|
||||
if (res.isNotEmpty) throw res;
|
||||
lastTunEnable = useClashConfig.tun.enable;
|
||||
lastProfileModified = await config.getCurrentProfile()?.profileLastModified;
|
||||
}
|
||||
|
||||
handleStart() async {
|
||||
handleStart([UpdateTasks? tasks]) async {
|
||||
await clashCore.startListener();
|
||||
if (globalState.isVpnService) {
|
||||
await vpn?.startVpn();
|
||||
startListenUpdate();
|
||||
return;
|
||||
}
|
||||
await service?.startVpn();
|
||||
startUpdateTasks(tasks);
|
||||
startTime ??= DateTime.now();
|
||||
await service?.init();
|
||||
startListenUpdate();
|
||||
}
|
||||
|
||||
restartCore({
|
||||
@@ -135,27 +131,28 @@ class GlobalState {
|
||||
required Config config,
|
||||
bool isPatch = true,
|
||||
}) async {
|
||||
await clashService?.startCore();
|
||||
await clashService?.reStart();
|
||||
await initCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
);
|
||||
|
||||
if (isStart) {
|
||||
await handleStart();
|
||||
}
|
||||
}
|
||||
|
||||
updateStartTime() {
|
||||
startTime = clashLib?.getRunTime();
|
||||
Future updateStartTime() async {
|
||||
startTime = await clashLib?.getRunTime();
|
||||
}
|
||||
|
||||
Future handleStop() async {
|
||||
startTime = null;
|
||||
await clashCore.stopListener();
|
||||
clashLib?.stopTun();
|
||||
await service?.destroy();
|
||||
stopListenUpdate();
|
||||
await service?.stopVpn();
|
||||
stopUpdateTasks();
|
||||
}
|
||||
|
||||
Future applyProfile({
|
||||
@@ -178,6 +175,35 @@ class GlobalState {
|
||||
appState.providers = await clashCore.getExternalProviders();
|
||||
}
|
||||
|
||||
CoreState getCoreState(Config config, ClashConfig clashConfig) {
|
||||
return 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,
|
||||
currentProfileName:
|
||||
config.currentProfile?.label ?? config.currentProfileId ?? "",
|
||||
routeAddress: clashConfig.routeAddress,
|
||||
);
|
||||
}
|
||||
|
||||
getUpdateConfigParams(Config config, ClashConfig clashConfig, bool isPatch) {
|
||||
return UpdateConfigParams(
|
||||
profileId: config.currentProfileId ?? "",
|
||||
config: clashConfig,
|
||||
params: ConfigExtendedParams(
|
||||
isPatch: isPatch,
|
||||
isCompatible: true,
|
||||
selectedMap: config.currentSelectedMap,
|
||||
overrideDns: config.overrideDns,
|
||||
testUrl: config.appSetting.testUrl,
|
||||
onlyStatisticsProxy: config.appSetting.onlyStatisticsProxy,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
init({
|
||||
required AppState appState,
|
||||
required Config config,
|
||||
@@ -190,18 +216,7 @@ class GlobalState {
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
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 ?? "",
|
||||
),
|
||||
getCoreState(config, clashConfig),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -210,13 +225,12 @@ class GlobalState {
|
||||
appState.groups = await clashCore.getProxiesGroups();
|
||||
}
|
||||
|
||||
showMessage({
|
||||
Future<bool?> showMessage<bool>({
|
||||
required String title,
|
||||
required InlineSpan message,
|
||||
Function()? onTab,
|
||||
String? confirmText,
|
||||
}) {
|
||||
showCommonDialog(
|
||||
}) async {
|
||||
return await showCommonDialog<bool>(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
@@ -238,10 +252,15 @@ class GlobalState {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: onTab ??
|
||||
() {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(appLocalizations.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(confirmText ?? appLocalizations.confirm),
|
||||
)
|
||||
],
|
||||
@@ -286,19 +305,10 @@ class GlobalState {
|
||||
required Config config,
|
||||
AppFlowingState? appFlowingState,
|
||||
}) async {
|
||||
final onlyProxy = config.appSetting.onlyProxy;
|
||||
final traffic = await clashCore.getTraffic(onlyProxy);
|
||||
if (Platform.isAndroid && isVpnService == true) {
|
||||
vpn?.startForeground(
|
||||
title: clashLib?.getCurrentProfileName() ?? "",
|
||||
content: "$traffic",
|
||||
);
|
||||
} else {
|
||||
if (appFlowingState != null) {
|
||||
appFlowingState.addTraffic(traffic);
|
||||
appFlowingState.totalTraffic =
|
||||
await clashCore.getTotalTraffic(onlyProxy);
|
||||
}
|
||||
final traffic = await clashCore.getTraffic();
|
||||
if (appFlowingState != null) {
|
||||
appFlowingState.addTraffic(traffic);
|
||||
appFlowingState.totalTraffic = await clashCore.getTotalTraffic();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,18 +336,22 @@ class GlobalState {
|
||||
}
|
||||
|
||||
showNotifier(String text) {
|
||||
if (text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
navigatorKey.currentContext?.showNotifier(text);
|
||||
}
|
||||
|
||||
openUrl(String url) {
|
||||
showMessage(
|
||||
openUrl(String url) async {
|
||||
final res = await showMessage(
|
||||
message: TextSpan(text: url),
|
||||
title: appLocalizations.externalLink,
|
||||
confirmText: appLocalizations.go,
|
||||
onTab: () {
|
||||
launchUrl(Uri.parse(url));
|
||||
},
|
||||
);
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
launchUrl(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ class CommonCard extends StatelessWidget {
|
||||
if (isSelected) {
|
||||
return colorScheme.secondaryContainer;
|
||||
}
|
||||
return colorScheme.surfaceContainerLow;
|
||||
return colorScheme.surfaceContainer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ class CommonCard extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (selectWidget != null && isSelected) {
|
||||
final List<Widget> children = [];
|
||||
children.add(childWidget);
|
||||
@@ -182,6 +182,9 @@ class CommonCard extends StatelessWidget {
|
||||
|
||||
return OutlinedButton(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
onLongPress: (){
|
||||
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
shape: WidgetStatePropertyAll(
|
||||
|
||||
263
lib/widgets/popup.dart
Normal file
263
lib/widgets/popup.dart
Normal file
@@ -0,0 +1,263 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommonPopupRoute<T> extends PopupRoute<T> {
|
||||
final WidgetBuilder builder;
|
||||
ValueNotifier<Offset> offsetNotifier;
|
||||
|
||||
CommonPopupRoute({
|
||||
required this.barrierLabel,
|
||||
required this.builder,
|
||||
required this.offsetNotifier,
|
||||
});
|
||||
|
||||
@override
|
||||
String? barrierLabel;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => true;
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return builder(
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||
Animation<double> secondaryAnimation, Widget child) {
|
||||
final align = Alignment.topRight;
|
||||
final animationValue = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeIn,
|
||||
).value;
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: offsetNotifier,
|
||||
builder: (_, value, child) {
|
||||
return Align(
|
||||
alignment: align,
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: OverflowAwareLayoutDelegate(
|
||||
offset: value.translate(
|
||||
48,
|
||||
12,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (_, Widget? child) {
|
||||
return Opacity(
|
||||
opacity: 0.1 + 0.9 * animationValue,
|
||||
child: Transform.scale(
|
||||
alignment: align,
|
||||
scale: 0.8 + 0.2 * animationValue,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -10) * (1 - animationValue),
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: builder(
|
||||
context,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 150);
|
||||
}
|
||||
|
||||
class CommonPopupBox extends StatefulWidget {
|
||||
final Widget target;
|
||||
final Widget popup;
|
||||
|
||||
const CommonPopupBox({
|
||||
super.key,
|
||||
required this.target,
|
||||
required this.popup,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommonPopupBox> createState() => _CommonPopupBoxState();
|
||||
}
|
||||
|
||||
class _CommonPopupBoxState extends State<CommonPopupBox> {
|
||||
final _targetKey = GlobalKey();
|
||||
final _targetOffsetValueNotifier = ValueNotifier(Offset.zero);
|
||||
|
||||
_handleTargetOffset() {
|
||||
final renderBox =
|
||||
_targetKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) {
|
||||
return;
|
||||
}
|
||||
_targetOffsetValueNotifier.value = renderBox.localToGlobal(
|
||||
Offset.zero,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerDown: (details) {
|
||||
_handleTargetOffset();
|
||||
Navigator.of(context).push(
|
||||
CommonPopupRoute(
|
||||
barrierLabel: other.id,
|
||||
builder: (BuildContext context) {
|
||||
return widget.popup;
|
||||
},
|
||||
offsetNotifier: _targetOffsetValueNotifier,
|
||||
),
|
||||
);
|
||||
},
|
||||
key: _targetKey,
|
||||
child: LayoutBuilder(
|
||||
builder: (_, __) {
|
||||
return widget.target;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OverflowAwareLayoutDelegate extends SingleChildLayoutDelegate {
|
||||
final Offset offset;
|
||||
|
||||
OverflowAwareLayoutDelegate({
|
||||
required this.offset,
|
||||
});
|
||||
|
||||
@override
|
||||
Size getSize(BoxConstraints constraints) {
|
||||
return Size(constraints.maxWidth, constraints.maxHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
Offset getPositionForChild(Size size, Size childSize) {
|
||||
final saveOffset = Offset(16, 16);
|
||||
double x = (offset.dx - childSize.width).clamp(
|
||||
0,
|
||||
size.width - saveOffset.dx - childSize.width,
|
||||
);
|
||||
double y = (offset.dy).clamp(
|
||||
0,
|
||||
size.height - saveOffset.dy - childSize.height,
|
||||
);
|
||||
return Offset(x, y);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(covariant OverflowAwareLayoutDelegate oldDelegate) {
|
||||
return oldDelegate.offset != offset;
|
||||
}
|
||||
}
|
||||
|
||||
class CommonPopupMenu extends StatelessWidget {
|
||||
final List<ActionItemData> items;
|
||||
|
||||
const CommonPopupMenu({
|
||||
super.key,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
Widget _popupMenuItem(
|
||||
BuildContext context, {
|
||||
required ActionItemData item,
|
||||
required int index,
|
||||
}) {
|
||||
final isDanger = item.type == ActionType.danger;
|
||||
final color = isDanger
|
||||
? context.colorScheme.error
|
||||
: context.colorScheme.onSurfaceVariant;
|
||||
return InkWell(
|
||||
hoverColor:
|
||||
isDanger ? context.colorScheme.errorContainer.withOpacity(0.3) : null,
|
||||
splashColor:
|
||||
isDanger ? context.colorScheme.errorContainer.withOpacity(0.4) : null,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
item.onPressed();
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 64,
|
||||
top: 14,
|
||||
bottom: 14,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (item.icon != null) ...[
|
||||
Icon(
|
||||
item.icon,
|
||||
size: item.iconSize ?? 18,
|
||||
color: color,
|
||||
),
|
||||
SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.label,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicHeight(
|
||||
child: IntrinsicWidth(
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final item in items.asMap().entries) ...[
|
||||
_popupMenuItem(
|
||||
context,
|
||||
item: item.value,
|
||||
index: item.key,
|
||||
),
|
||||
if (item.value != items.last)
|
||||
Divider(
|
||||
height: 0,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommonPopupMenuItem<T> {
|
||||
T action;
|
||||
String label;
|
||||
IconData? iconData;
|
||||
|
||||
CommonPopupMenuItem({
|
||||
required this.action,
|
||||
required this.label,
|
||||
this.iconData,
|
||||
});
|
||||
}
|
||||
|
||||
class CommonPopupMenu<T> extends StatefulWidget {
|
||||
final List<CommonPopupMenuItem> items;
|
||||
final PopupMenuItemSelected<T> onSelected;
|
||||
final T? selectedValue;
|
||||
final Widget? icon;
|
||||
|
||||
const CommonPopupMenu({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
this.icon,
|
||||
}) : selectedValue = null;
|
||||
|
||||
const CommonPopupMenu.radio({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
required T this.selectedValue,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommonPopupMenu<T>> createState() => _CommonPopupMenuState();
|
||||
}
|
||||
|
||||
class _CommonPopupMenuState<T> extends State<CommonPopupMenu<T>> {
|
||||
late ValueNotifier<T?> groupValue;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
groupValue = ValueNotifier(widget.selectedValue);
|
||||
}
|
||||
|
||||
handleSelect(T value) {
|
||||
if (widget.selectedValue != null) {
|
||||
this.groupValue.value = value;
|
||||
}
|
||||
widget.onSelected(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
groupValue.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<T>(
|
||||
icon: widget.icon,
|
||||
onSelected: handleSelect,
|
||||
itemBuilder: (_) {
|
||||
return [
|
||||
for (final item in widget.items)
|
||||
PopupMenuItem<T>(
|
||||
value: item.action,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
item.iconData != null
|
||||
? Flexible(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
child: Icon(item.iconData),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: SizedBox(
|
||||
child: Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.selectedValue != null)
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: ValueListenableBuilder<T?>(
|
||||
valueListenable: groupValue,
|
||||
builder: (_, groupValue, __) {
|
||||
return Radio<T>(
|
||||
value: item.action,
|
||||
groupValue: groupValue,
|
||||
onChanged: (T? value) {
|
||||
if (value != null) {
|
||||
handleSelect(value);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export 'line_chart.dart';
|
||||
export 'list.dart';
|
||||
export 'null_status.dart';
|
||||
export 'open_container.dart';
|
||||
export 'popup_menu.dart';
|
||||
export 'popup.dart';
|
||||
export 'scaffold.dart';
|
||||
export 'setting.dart';
|
||||
export 'sheet.dart';
|
||||
|
||||
Reference in New Issue
Block a user