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:
chen08209
2025-01-13 19:08:17 +08:00
parent 6a39b7ef5a
commit b340feeb49
92 changed files with 4000 additions and 3081 deletions

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -34,3 +34,4 @@ export 'text.dart';
export 'tray.dart';
export 'window.dart';
export 'windows.dart';
export 'render.dart';

View File

@@ -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";

View File

@@ -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();
}

View File

@@ -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
View 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;

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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 }

View File

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

View File

@@ -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(

View File

@@ -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(

View File

@@ -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();
});
});
}

View File

@@ -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,
),
),
),
),

View File

@@ -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,
),
),
),

View File

@@ -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,

View File

@@ -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!);
}
}

View File

@@ -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(

View File

@@ -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();

View File

@@ -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,
),

View File

@@ -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();
}

View File

@@ -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}'),

View File

@@ -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();

View File

@@ -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"
}

View File

@@ -333,5 +333,13 @@
"routeAddressDesc": "配置监听路由地址",
"pleaseInputAdminPassword": "请输入管理员密码",
"copyEnvVar": "复制环境变量",
"memoryInfo": "内存信息"
"memoryInfo": "内存信息",
"cancel": "取消",
"fileIsUpdate": "文件有修改,是否保存修改",
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
"hasCacheChange": "是否缓存修改",
"nullProxies": "暂无代理",
"copySuccess": "复制成功",
"copyLink": "复制链接",
"exportFile": "导出文件"
}

View File

@@ -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":

View File

@@ -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":

View File

@@ -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> {

View File

@@ -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);
}
}

View File

@@ -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);

View File

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

View File

@@ -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);

View File

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

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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
View 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!);
}
}

View File

@@ -1,2 +1,3 @@
export 'home.dart';
export 'scan.dart';
export 'scan.dart';
export 'editor.dart';

View File

@@ -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;

View File

@@ -15,7 +15,6 @@ abstract mixin class TileListener {
}
class Tile {
StreamSubscription? subscription;
final MethodChannel _channel = const MethodChannel('tile');

View File

@@ -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;

View File

@@ -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));
}
}

View File

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

View File

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

View File

@@ -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';